diff --git a/app/src/main/java/me/ash/reader/DataStoreExt.kt b/app/src/main/java/me/ash/reader/DataStoreExt.kt index d02747d..8d67d13 100644 --- a/app/src/main/java/me/ash/reader/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/DataStoreExt.kt @@ -15,6 +15,8 @@ import kotlinx.coroutines.runBlocking import java.io.IOException val Context.dataStore: DataStore by preferencesDataStore(name = "settings") +val Context.currentAccountId: Int + get() = this.dataStore.get(DataStoreKeys.CurrentAccountId)!! suspend fun DataStore.put(dataStoreKeys: DataStoreKeys, value: T) { this.edit { diff --git a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt index 236c435..ed43bc2 100644 --- a/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/data/article/ArticleDao.kt @@ -7,6 +7,15 @@ import kotlinx.coroutines.flow.Flow @Dao interface ArticleDao { + @Query( + """ + DELETE FROM article + WHERE accountId = :accountId + AND feedId = :feedId + """ + ) + suspend fun deleteByFeedId(accountId: Int, feedId: String) + @Transaction @Query( """ diff --git a/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt b/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt index 7dce150..3be1c97 100644 --- a/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt +++ b/app/src/main/java/me/ash/reader/data/feed/FeedDao.kt @@ -4,6 +4,14 @@ import androidx.room.* @Dao interface FeedDao { + @Query( + """ + SELECT * FROM feed + WHERE id = :id + """ + ) + suspend fun queryById(id: String): Feed? + @Query( """ SELECT * FROM feed diff --git a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt index d340895..384c048 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AbstractRssRepository.kt @@ -8,13 +8,10 @@ import androidx.work.* import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext -import me.ash.reader.DataStoreKeys +import me.ash.reader.currentAccountId import me.ash.reader.data.account.AccountDao import me.ash.reader.data.article.Article import me.ash.reader.data.article.ArticleDao @@ -26,8 +23,6 @@ import me.ash.reader.data.group.Group import me.ash.reader.data.group.GroupDao import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.data.source.RssNetworkDataSource -import me.ash.reader.dataStore -import me.ash.reader.get import java.util.concurrent.TimeUnit abstract class AbstractRssRepository constructor( @@ -65,14 +60,11 @@ abstract class AbstractRssRepository constructor( } fun pullGroups(): Flow> { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 - return groupDao.queryAllGroup(accountId) + return groupDao.queryAllGroup(context.currentAccountId).flowOn(Dispatchers.IO) } fun pullFeeds(): Flow> { - return groupDao.queryAllGroupWithFeed( - context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 - )//.flowOn(Dispatchers.IO) + return groupDao.queryAllGroupWithFeed(context.currentAccountId).flowOn(Dispatchers.IO) } fun pullArticles( @@ -82,7 +74,7 @@ abstract class AbstractRssRepository constructor( isUnread: Boolean = false, ): PagingSource { Log.i("RLog", "thread:pullArticles ${Thread.currentThread().name}") - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 + val accountId = context.currentAccountId Log.i( "RLog", "pullArticles: accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}" @@ -118,7 +110,7 @@ abstract class AbstractRssRepository constructor( ): Flow> { return withContext(Dispatchers.IO) { Log.i("RLog", "thread:pullImportant ${Thread.currentThread().name}") - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! + val accountId = context.currentAccountId Log.i( "RLog", "pullImportant: accountId: ${accountId}, isStarred: ${isStarred}, isUnread: ${isUnread}" @@ -130,7 +122,11 @@ abstract class AbstractRssRepository constructor( .queryImportantCountWhenIsUnread(accountId, isUnread) else -> articleDao.queryImportantCountWhenIsAll(accountId) } - }//.flowOn(Dispatchers.IO) + }.flowOn(Dispatchers.IO) + } + + suspend fun findFeedById(id: String): Feed? { + return feedDao.queryById(id) } suspend fun findArticleById(id: String): ArticleWithFeed? { @@ -138,14 +134,32 @@ abstract class AbstractRssRepository constructor( } suspend fun isExist(url: String): Boolean { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! - return feedDao.queryByLink(accountId, url).isNotEmpty() + return feedDao.queryByLink(context.currentAccountId, url).isNotEmpty() } fun peekWork(): String { return workManager.getWorkInfosByTag("sync").get().size.toString() } + suspend fun updateGroup(group: Group) { + groupDao.update(group) + } + + suspend fun updateFeed(feed: Feed) { + feedDao.update(feed) + } + + suspend fun deleteGroup(group: Group) { + groupDao.update(group) + } + + suspend fun deleteFeed(feed: Feed) { + withContext(Dispatchers.IO) { + articleDao.deleteByFeedId(context.currentAccountId, feed.id) + feedDao.delete(feed) + } + } + companion object { val mutex = Mutex() diff --git a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt index a799ff5..378d991 100644 --- a/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/AccountRepository.kt @@ -2,11 +2,13 @@ package me.ash.reader.data.repository import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import me.ash.reader.* +import me.ash.reader.R +import me.ash.reader.currentAccountId import me.ash.reader.data.account.Account import me.ash.reader.data.account.AccountDao import me.ash.reader.data.group.Group import me.ash.reader.data.group.GroupDao +import me.ash.reader.spacerDollar import javax.inject.Inject class AccountRepository @Inject constructor( @@ -17,8 +19,7 @@ class AccountRepository @Inject constructor( ) { suspend fun getCurrentAccount(): Account? { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 - return accountDao.queryById(accountId) + return accountDao.queryById(context.currentAccountId) } suspend fun isNoAccount(): Boolean { diff --git a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt index e749965..b26cc60 100644 --- a/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/FeverRssRepository.kt @@ -5,7 +5,7 @@ import android.util.Log import androidx.work.WorkManager import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.sync.withLock -import me.ash.reader.DataStoreKeys +import me.ash.reader.* import me.ash.reader.data.account.AccountDao import me.ash.reader.data.article.Article import me.ash.reader.data.article.ArticleDao @@ -15,9 +15,6 @@ import me.ash.reader.data.group.Group import me.ash.reader.data.group.GroupDao import me.ash.reader.data.source.FeverApiDataSource import me.ash.reader.data.source.RssNetworkDataSource -import me.ash.reader.dataStore -import me.ash.reader.get -import me.ash.reader.spacerDollar import net.dankito.readability4j.extended.Readability4JExtended import java.util.* import javax.inject.Inject @@ -49,13 +46,12 @@ class FeverRssRepository @Inject constructor( } override suspend fun addGroup(name: String): String { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! return UUID.randomUUID().toString().also { groupDao.insert( Group( id = it, name = name, - accountId = accountId + accountId = context.currentAccountId ) ) } @@ -63,8 +59,7 @@ class FeverRssRepository @Inject constructor( override suspend fun sync() { mutex.withLock { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) - ?: return + val accountId = context.currentAccountId updateSyncState { it.copy( diff --git a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt index a850cd5..91c17e7 100644 --- a/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/LocalRssRepository.kt @@ -73,13 +73,12 @@ class LocalRssRepository @Inject constructor( } override suspend fun addGroup(name: String): String { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! return UUID.randomUUID().toString().also { groupDao.insert( Group( id = it, name = name, - accountId = accountId + accountId = context.currentAccountId ) ) } @@ -89,8 +88,7 @@ class LocalRssRepository @Inject constructor( mutex.withLock { withContext(Dispatchers.IO) { val preTime = System.currentTimeMillis() - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) - ?: return@withContext + val accountId = context.currentAccountId val feeds = async { feedDao.queryAll(accountId) } val articles = feeds.await().also { feed -> updateSyncState { diff --git a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt index a5ac99b..9c458ee 100644 --- a/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt +++ b/app/src/main/java/me/ash/reader/data/repository/RssHelper.kt @@ -3,14 +3,12 @@ package me.ash.reader.data.repository import android.content.Context import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext -import me.ash.reader.DataStoreKeys +import me.ash.reader.currentAccountId import me.ash.reader.data.article.Article import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.FeedDao import me.ash.reader.data.feed.FeedWithArticle import me.ash.reader.data.source.RssNetworkDataSource -import me.ash.reader.dataStore -import me.ash.reader.get import me.ash.reader.spacerDollar import net.dankito.readability4j.Readability4J import net.dankito.readability4j.extended.Readability4JExtended @@ -26,7 +24,7 @@ class RssHelper @Inject constructor( ) { @Throws(Exception::class) suspend fun searchFeed(feedLink: String): FeedWithArticle { - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 + val accountId = context.currentAccountId val parseRss = rssNetworkDataSource.parseRss(feedLink) val feed = Feed( id = accountId.spacerDollar(UUID.randomUUID().toString()), diff --git a/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt b/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt index c1de1d3..1c1f43d 100644 --- a/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt +++ b/app/src/main/java/me/ash/reader/data/source/OpmlLocalDataSource.kt @@ -4,12 +4,10 @@ import android.content.Context import android.util.Log import android.util.Xml import dagger.hilt.android.qualifiers.ApplicationContext -import me.ash.reader.DataStoreKeys +import me.ash.reader.currentAccountId import me.ash.reader.data.feed.Feed import me.ash.reader.data.group.Group import me.ash.reader.data.group.GroupWithFeed -import me.ash.reader.dataStore -import me.ash.reader.get import org.xmlpull.v1.XmlPullParser import java.io.InputStream import java.util.* @@ -22,7 +20,7 @@ class OpmlLocalDataSource @Inject constructor( // @Throws(XmlPullParserException::class, IOException::class) fun parseFileInputStream(inputStream: InputStream): List { val groupWithFeedList = mutableListOf() - val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 + val accountId = context.currentAccountId inputStream.use { val parser: XmlPullParser = Xml.newPullParser() parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) diff --git a/app/src/main/java/me/ash/reader/ui/extension/StateFlowExt.kt b/app/src/main/java/me/ash/reader/ui/extension/StateFlowExt.kt index a67aa95..7708bc7 100644 --- a/app/src/main/java/me/ash/reader/ui/extension/StateFlowExt.kt +++ b/app/src/main/java/me/ash/reader/ui/extension/StateFlowExt.kt @@ -1,12 +1,23 @@ package me.ash.reader.ui.extension import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KProperty @Composable fun StateFlow.collectAsStateValue( - context: CoroutineContext = EmptyCoroutineContext + context: CoroutineContext = Dispatchers.Default ): T = collectAsState(context).value + +@Suppress("NOTHING_TO_INLINE") +inline operator fun State.getValue(thisObj: Any?, property: KProperty<*>): T = value + +@Suppress("NOTHING_TO_INLINE") +inline operator fun MutableState.setValue(thisObj: Any?, property: KProperty<*>, value: T) { + this.value = value +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt index bfc4847..ade57d5 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt @@ -6,15 +6,12 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.* -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.google.accompanist.insets.ProvideWindowInsets @@ -24,16 +21,10 @@ import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController -import me.ash.reader.ui.page.home.FeedOptionDrawer import me.ash.reader.ui.page.home.HomePage import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.theme.AppTheme -@OptIn(ExperimentalMaterialApi::class) -val LocalDrawerState = staticCompositionLocalOf { - error("CompositionLocal LocalDrawerState not present") -} - @OptIn(ExperimentalAnimationApi::class, androidx.compose.material.ExperimentalMaterialApi::class) @Composable fun HomeEntry() { @@ -46,113 +37,105 @@ fun HomeEntry() { setSystemBarsColor(Color.Transparent, !isSystemInDarkTheme()) setNavigationBarColor(MaterialTheme.colorScheme.surface, !isSystemInDarkTheme()) } - Box { - CompositionLocalProvider( - LocalDrawerState provides rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) - ) { - Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { - Row( - modifier = Modifier - .weight(1f) - .statusBarsPadding() + Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { + Row( + modifier = Modifier + .weight(1f) + .statusBarsPadding() + ) { + AnimatedNavHost( + navController = navController, + startDestination = RouteName.HOME, ) { - AnimatedNavHost( - modifier = Modifier.background(MaterialTheme.colorScheme.surface), - navController = navController, - startDestination = RouteName.HOME, + composable( + route = RouteName.HOME, + enterTransition = { + slideInHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + initialOffsetX = { -it } + ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) + }, + exitTransition = { + slideOutHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + targetOffsetX = { it } + ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) + }, + popEnterTransition = { + slideInHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + initialOffsetX = { -it } + ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) + }, + popExitTransition = { + slideOutHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + targetOffsetX = { it } + ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) + }, ) { - composable( - route = RouteName.HOME, - enterTransition = { - slideInHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - initialOffsetX = { -it } - ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) - }, - exitTransition = { - slideOutHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - targetOffsetX = { it } - ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) - }, - popEnterTransition = { - slideInHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - initialOffsetX = { -it } - ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) - }, - popExitTransition = { - slideOutHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - targetOffsetX = { it } - ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) - }, - ) { - HomePage(navController) - } - composable( - route = RouteName.SETTINGS, - enterTransition = { - slideInHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - initialOffsetX = { -it } - ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) - }, - exitTransition = { - slideOutHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - targetOffsetX = { -it } - ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) - }, - popEnterTransition = { - slideInHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - initialOffsetX = { -it } - ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) - }, - popExitTransition = { - slideOutHorizontally( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - dampingRatio = Spring.DampingRatioNoBouncy - ), - targetOffsetX = { -it } - ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) - }, - ) { - SettingsPage(navController) - } + HomePage(navController) + } + composable( + route = RouteName.SETTINGS, + enterTransition = { + slideInHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + initialOffsetX = { -it } + ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) + }, + exitTransition = { + slideOutHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + targetOffsetX = { -it } + ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) + }, + popEnterTransition = { + slideInHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + initialOffsetX = { -it } + ) + fadeIn(animationSpec = tween(220, delayMillis = 90)) + }, + popExitTransition = { + slideOutHorizontally( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + dampingRatio = Spring.DampingRatioNoBouncy + ), + targetOffsetX = { -it } + ) + fadeOut(animationSpec = tween(220, delayMillis = 90)) + }, + ) { + SettingsPage(navController) } } - Spacer( - modifier = Modifier - .navigationBarsHeight() - .fillMaxWidth() - ) } - FeedOptionDrawer(drawerState = LocalDrawerState.current) - } + Spacer( + modifier = Modifier + .navigationBarsHeight() + .fillMaxWidth() + ) } } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/FeedOptionDrawer.kt deleted file mode 100644 index f99e30a..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/FeedOptionDrawer.kt +++ /dev/null @@ -1,116 +0,0 @@ -package me.ash.reader.ui.page.home - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ChipDefaults -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.RssFeed -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import me.ash.reader.R -import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage -import me.ash.reader.ui.widget.BottomDrawer -import me.ash.reader.ui.widget.Subtitle - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun FeedOptionDrawer( - modifier: Modifier = Modifier, - drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden - ), -) { - BottomDrawer( - drawerState = drawerState, - ) { - Column { - Icon( - modifier = modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), - imageVector = Icons.Rounded.RssFeed, - contentDescription = stringResource(R.string.subscribe), - ) - Spacer(modifier = modifier.height(16.dp)) - Text( - modifier = Modifier.fillMaxWidth(), - text = "Feed", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center, - ) - Spacer(modifier = modifier.height(16.dp)) - ResultViewPage( - link = "https://joeycz.github.io/weekly/rss.xml", - groups = emptyList(), - selectedAllowNotificationPreset = true, - selectedParseFullContentPreset = true, - selectedGroupId = "selectedGroupId", - newGroupContent = "", - onNewGroupValueChange = { }, - newGroupSelected = false, - changeNewGroupSelected = { }, - allowNotificationPresetOnClick = { }, - parseFullContentPresetOnClick = { }, - groupOnClick = { }, - onKeyboardAction = { }, - ) - Spacer(modifier = Modifier.height(20.dp)) - Subtitle(text = "More") - Spacer(modifier = Modifier.height(10.dp)) - androidx.compose.material.FilterChip( - modifier = modifier, - colors = ChipDefaults.filterChipColors( - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.error, - leadingIconColor = MaterialTheme.colorScheme.error, - disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy( - alpha = 0.7f - ), - disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy( - alpha = 0.7f - ), - selectedBackgroundColor = Color.Transparent, - selectedContentColor = MaterialTheme.colorScheme.error, - selectedLeadingIconColor = MaterialTheme.colorScheme.error - ), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), - selected = false, - shape = CircleShape, - onClick = { -// focusManager.clearFocus() -// onClick() - }, - content = { - Text( - modifier = modifier.padding( - start = if (false) 0.dp else 8.dp, - top = 8.dp, - end = 8.dp, - bottom = 8.dp - ), - text = "Delete", - style = MaterialTheme.typography.titleSmall, - color = if (true) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.error - }, - ) - }, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt index c5f2218..2487ee9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/FilterBar2.kt @@ -29,7 +29,7 @@ fun FilterBar2( NavigationBarItem( icon = { Icon( - imageVector = if (filter == item) item.filledIcon else item.icon, + imageVector = item.icon, contentDescription = item.getName() ) }, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt index 5b355f6..4616937 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeBottomNavBar.kt @@ -17,10 +17,14 @@ import androidx.compose.material.ChipDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FilterChip import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FiberManualRecord import androidx.compose.material.icons.outlined.Article -import androidx.compose.material.icons.outlined.Circle -import androidx.compose.material.icons.outlined.Sell -import androidx.compose.material.icons.rounded.* +import androidx.compose.material.icons.outlined.FiberManualRecord +import androidx.compose.material.icons.outlined.TextFormat +import androidx.compose.material.icons.rounded.Article +import androidx.compose.material.icons.rounded.ExpandMore +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -138,7 +142,7 @@ fun HomeBottomNavBar( .alpha(readerBarAlpha), ) { ReaderBar( - modifier = modifier, + modifier = Modifier, disabled = disabled, isUnread = isUnread, isStarred = isStarred, @@ -323,67 +327,78 @@ private fun ReaderBar( val view = LocalView.current var fullContent by remember { mutableStateOf(isFullContent) } Row( - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) + modifier = modifier.fillMaxWidth() ) { CanBeDisabledIconButton( - modifier = Modifier.size(18.dp), + modifier = Modifier.size(40.dp), disabled = disabled, imageVector = if (isUnread) { - Icons.Rounded.Circle + Icons.Filled.FiberManualRecord } else { - Icons.Outlined.Circle + Icons.Outlined.FiberManualRecord }, contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread), - tint = MaterialTheme.colorScheme.primary, + tint = if (isUnread) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.outline + }, ) { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) unreadOnClick(!isUnread) } CanBeDisabledIconButton( + modifier = Modifier.size(40.dp), disabled = disabled, - modifier = Modifier.size(28.dp), imageVector = if (isStarred) { Icons.Rounded.Star } else { - Icons.Rounded.StarBorder + Icons.Rounded.StarOutline }, contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred), - tint = MaterialTheme.colorScheme.primary, + tint = if (isStarred) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.outline + }, ) { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) starredOnClick(!isStarred) } CanBeDisabledIconButton( disabled = disabled, - modifier = Modifier.size(30.dp), + modifier = Modifier.size(40.dp), imageVector = Icons.Rounded.ExpandMore, contentDescription = "Next Article", - tint = MaterialTheme.colorScheme.primary, + tint = MaterialTheme.colorScheme.outline, ) { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) } CanBeDisabledIconButton( + modifier = Modifier.size(40.dp), disabled = disabled, - imageVector = Icons.Outlined.Sell, + imageVector = Icons.Outlined.TextFormat, contentDescription = "Add Tag", - tint = MaterialTheme.colorScheme.primary, + tint = MaterialTheme.colorScheme.outline, ) { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) } CanBeDisabledIconButton( disabled = disabled, - modifier = Modifier.size(26.dp), + modifier = Modifier.size(40.dp), imageVector = if (fullContent) { Icons.Rounded.Article } else { Icons.Outlined.Article }, contentDescription = stringResource(R.string.parse_full_content), - tint = MaterialTheme.colorScheme.primary, + tint = if (fullContent) { + MaterialTheme.colorScheme.onSecondaryContainer + } else { + MaterialTheme.colorScheme.outline + }, ) { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) val afterIsFullContent = !fullContent diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt index c40f4a3..aea1d49 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomePage.kt @@ -2,12 +2,9 @@ package me.ash.reader.ui.page.home import android.util.Log import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -17,11 +14,13 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.google.accompanist.pager.ExperimentalPagerApi -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.findActivity import me.ash.reader.ui.page.common.ExtraName +import me.ash.reader.ui.page.home.drawer.feed.FeedOptionDrawer +import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction +import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel import me.ash.reader.ui.page.home.feeds.FeedsPage import me.ash.reader.ui.page.home.flow.FlowPage import me.ash.reader.ui.page.home.read.ReadPage @@ -35,13 +34,13 @@ fun HomePage( navController: NavHostController, viewModel: HomeViewModel = hiltViewModel(), readViewModel: ReadViewModel = hiltViewModel(), + feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), ) { val context = LocalContext.current val viewState = viewModel.viewState.collectAsStateValue() val filterState = viewModel.filterState.collectAsStateValue() val readState = readViewModel.viewState.collectAsStateValue() val scope = rememberCoroutineScope() - val drawerState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) LaunchedEffect(Unit) { context.findActivity()?.let { activity -> @@ -82,6 +81,9 @@ fun HomePage( if (currentPage == 2) { readViewModel.dispatch(ReadViewAction.ClearArticle) } + if (currentPage == 0) { + feedOptionViewModel.dispatch(FeedOptionViewAction.Hide(scope)) + } } ) ) @@ -96,69 +98,55 @@ fun HomePage( } } - Box { - Column { - ViewPager( - modifier = Modifier.weight(1f), - state = viewState.pagerState, - composableList = listOf( - { - FeedsPage(navController = navController) - }, - { - FlowPage(navController = navController) - }, - { - ReadPage( - navController = navController, - btnBackOnClickListener = { - viewModel.dispatch( - HomeViewAction.ScrollToPage( - scope = scope, - targetPage = 1, - callback = { - readViewModel.dispatch(ReadViewAction.ClearArticle) - } - ) - ) - }, - ) - }, - ), - ) - HomeBottomNavBar( - modifier = Modifier - .height(60.dp) - .fillMaxWidth(), - pagerState = viewState.pagerState, - disabled = readState.articleWithFeed == null, - isUnread = readState.articleWithFeed?.article?.isUnread ?: false, - isStarred = readState.articleWithFeed?.article?.isStarred ?: false, - isFullContent = readState.articleWithFeed?.feed?.isFullContent ?: false, - unreadOnClick = { - readViewModel.dispatch(ReadViewAction.MarkUnread(it)) + Column { + ViewPager( + modifier = Modifier.weight(1f), + state = viewState.pagerState, + composableList = listOf( + { + FeedsPage(navController = navController) }, - starredOnClick = { - readViewModel.dispatch(ReadViewAction.MarkStarred(it)) + { + FlowPage(navController = navController) }, - fullContentOnClick = { afterIsFullContent -> - readState.articleWithFeed?.let { - if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) - else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) - } + { + ReadPage(navController = navController) }, - filter = filterState.filter, - filterOnClick = { - viewModel.dispatch( - HomeViewAction.ChangeFilter( - filterState.copy( - filter = it - ) + ), + ) + HomeBottomNavBar( + modifier = Modifier + .height(60.dp) + .fillMaxWidth(), + pagerState = viewState.pagerState, + disabled = readState.articleWithFeed == null, + isUnread = readState.articleWithFeed?.article?.isUnread ?: false, + isStarred = readState.articleWithFeed?.article?.isStarred ?: false, + isFullContent = readState.articleWithFeed?.feed?.isFullContent ?: false, + unreadOnClick = { + readViewModel.dispatch(ReadViewAction.MarkUnread(it)) + }, + starredOnClick = { + readViewModel.dispatch(ReadViewAction.MarkStarred(it)) + }, + fullContentOnClick = { afterIsFullContent -> + readState.articleWithFeed?.let { + if (afterIsFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) + else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) + } + }, + filter = filterState.filter, + filterOnClick = { + viewModel.dispatch( + HomeViewAction.ChangeFilter( + filterState.copy( + filter = it ) ) - }, - ) - } - FeedOptionDrawer(drawerState = drawerState) + ) + }, + ) } + + FeedOptionDrawer() } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/DeleteFeedDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/DeleteFeedDialog.kt new file mode 100644 index 0000000..4ced62d --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/DeleteFeedDialog.kt @@ -0,0 +1,76 @@ +package me.ash.reader.ui.page.home.drawer.feed + +import android.widget.Toast +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.DeleteOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.pager.ExperimentalPagerApi +import me.ash.reader.R +import me.ash.reader.ui.extension.collectAsStateValue +import me.ash.reader.ui.widget.Dialog + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun DeleteFeedDialog( + modifier: Modifier = Modifier, + feedName: String, + viewModel: FeedOptionViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val viewState = viewModel.viewState.collectAsStateValue() + val scope = rememberCoroutineScope() + val deletedTip = stringResource(R.string.has_been_deleted, feedName) + + Dialog( + visible = viewState.deleteDialogVisible, + onDismissRequest = { + viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) + }, + icon = { + Icon( + imageVector = Icons.Rounded.DeleteOutline, + contentDescription = stringResource(R.string.subscribe), + ) + }, + title = { + Text(text = stringResource(R.string.unsubscribe)) + }, + text = { + Text(text = stringResource(R.string.unsubscribe_tip, feedName)) + }, + confirmButton = { + TextButton( + onClick = { + viewModel.dispatch(FeedOptionViewAction.Delete(){ + viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) + viewModel.dispatch(FeedOptionViewAction.Hide(scope)) + Toast.makeText(context, deletedTip, Toast.LENGTH_SHORT).show() + }) + } + ) { + Text( + text = stringResource(R.string.unsubscribe), + ) + } + }, + dismissButton = { + TextButton( + onClick = { + viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) + } + ) { + Text( + text = stringResource(R.string.cancel), + ) + } + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt new file mode 100644 index 0000000..f5c738c --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionDrawer.kt @@ -0,0 +1,124 @@ +package me.ash.reader.ui.page.home.drawer.feed + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.* +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.DeleteOutline +import androidx.compose.material.icons.rounded.RssFeed +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import me.ash.reader.R +import me.ash.reader.ui.extension.collectAsStateValue +import me.ash.reader.ui.extension.roundClick +import me.ash.reader.ui.page.home.feeds.subscribe.ResultViewPage +import me.ash.reader.ui.widget.BottomDrawer +import me.ash.reader.ui.widget.Subtitle + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun FeedOptionDrawer( + modifier: Modifier = Modifier, + viewModel: FeedOptionViewModel = hiltViewModel(), + content: @Composable () -> Unit = {}, +) { + val context = LocalContext.current + val viewState = viewModel.viewState.collectAsStateValue() + val feed = viewState.feed + + BottomDrawer( + drawerState = viewState.drawerState, + sheetContent = { + Column { + Icon( + modifier = modifier + .roundClick { } + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + imageVector = Icons.Rounded.RssFeed, + contentDescription = feed?.name + ?: stringResource(R.string.unknown), + tint = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = modifier.height(16.dp)) + Text( + modifier = Modifier + .roundClick {} + .fillMaxWidth(), + text = feed?.name ?: stringResource(R.string.unknown), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = modifier.height(16.dp)) + ResultViewPage( + link = feed?.url ?: stringResource(R.string.unknown), + groups = viewState.groups, + selectedAllowNotificationPreset = viewState.feed?.isNotification ?: false, + selectedParseFullContentPreset = viewState.feed?.isFullContent ?: false, + selectedGroupId = viewState.feed?.groupId ?: "", + newGroupContent = viewState.newGroupContent, + onNewGroupValueChange = { + viewModel.dispatch(FeedOptionViewAction.InputNewGroup(it)) + }, + newGroupSelected = viewState.newGroupSelected, + changeNewGroupSelected = { + viewModel.dispatch(FeedOptionViewAction.SelectedNewGroup(it)) + }, + allowNotificationPresetOnClick = { + viewModel.dispatch(FeedOptionViewAction.ChangeAllowNotificationPreset) + }, + parseFullContentPresetOnClick = { + viewModel.dispatch(FeedOptionViewAction.ChangeParseFullContentPreset) + }, + groupOnClick = { + viewModel.dispatch(FeedOptionViewAction.SelectedGroup(it)) + }, + onKeyboardAction = { }, + ) + Spacer(modifier = Modifier.height(20.dp)) + Subtitle(text = stringResource(R.string.options)) + Spacer(modifier = Modifier.height(10.dp)) + Button( + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.error, + ), + border = BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.error, + ), + onClick = { + viewModel.dispatch(FeedOptionViewAction.ShowDeleteDialog) + } + ) { + Icon( + modifier = Modifier.size(ButtonDefaults.IconSize), + imageVector = Icons.Rounded.DeleteOutline, + contentDescription = stringResource(R.string.delete), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text( + text = stringResource(R.string.unsubscribe), + style = MaterialTheme.typography.titleSmall, + ) + } + } + } + ) { + content() + } + + DeleteFeedDialog(feedName = feed?.name ?: "") +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionViewModel.kt new file mode 100644 index 0000000..6e95494 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/feed/FeedOptionViewModel.kt @@ -0,0 +1,206 @@ +package me.ash.reader.ui.page.home.drawer.feed + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.accompanist.pager.ExperimentalPagerApi +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import me.ash.reader.data.feed.Feed +import me.ash.reader.data.group.Group +import me.ash.reader.data.repository.RssRepository +import javax.inject.Inject + +@OptIn( + ExperimentalPagerApi::class, + ExperimentalMaterialApi::class +) +@HiltViewModel +class FeedOptionViewModel @Inject constructor( + private val rssRepository: RssRepository, +) : ViewModel() { + private val _viewState = MutableStateFlow(FeedOptionViewState()) + val viewState: StateFlow = _viewState.asStateFlow() + + init { + viewModelScope.launch { + rssRepository.get().pullGroups().collect { groups -> + _viewState.update { + it.copy( + groups = groups + ) + } + } + } + } + + fun dispatch(action: FeedOptionViewAction) { + when (action) { + is FeedOptionViewAction.Show -> show(action.scope, action.feedId) + is FeedOptionViewAction.Hide -> hide(action.scope) + is FeedOptionViewAction.SelectedGroup -> selectedGroup(action.groupId) + is FeedOptionViewAction.InputNewGroup -> inputNewGroup(action.content) + is FeedOptionViewAction.SelectedNewGroup -> selectedNewGroup(action.selected) + is FeedOptionViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset() + is FeedOptionViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset() + is FeedOptionViewAction.ShowDeleteDialog -> showDeleteDialog() + is FeedOptionViewAction.HideDeleteDialog -> hideDeleteDialog() + is FeedOptionViewAction.Delete -> delete(action.callback) + } + } + + private suspend fun fetchFeed(feedId: String) { + val feed = rssRepository.get().findFeedById(feedId) + _viewState.update { + it.copy( + feed = feed, + selectedGroupId = feed?.groupId ?: "", + ) + } + } + + private fun show(scope: CoroutineScope, feedId: String) { + scope.launch { + fetchFeed(feedId) + _viewState.value.drawerState.show() + } + } + + private fun hide(scope: CoroutineScope) { + scope.launch { + _viewState.value.drawerState.hide() + } + } + + private fun inputNewGroup(content: String) { + _viewState.update { + it.copy( + newGroupContent = content + ) + } + } + + private fun selectedGroup(groupId: String) { + viewModelScope.launch { + _viewState.value.feed?.let { + rssRepository.get().updateFeed( + it.copy( + groupId = groupId + ) + ) + fetchFeed(it.id) + } + } + } + + private fun selectedNewGroup(selected: Boolean) { + _viewState.update { + it.copy( + newGroupSelected = selected, + ) + } + } + + private fun changeParseFullContentPreset() { + viewModelScope.launch { + _viewState.value.feed?.let { + rssRepository.get().updateFeed( + it.copy( + isFullContent = !it.isFullContent + ) + ) + fetchFeed(it.id) + } + } + } + + private fun changeAllowNotificationPreset() { + viewModelScope.launch { + _viewState.value.feed?.let { + rssRepository.get().updateFeed( + it.copy( + isNotification = !it.isNotification + ) + ) + fetchFeed(it.id) + } + } + } + + private fun delete(callback: () -> Unit = {}) { + _viewState.value.feed?.let { + viewModelScope.launch { + rssRepository.get().deleteFeed(it) + }.invokeOnCompletion { + callback() + } + } + } + + private fun hideDeleteDialog() { + _viewState.update { + it.copy( + deleteDialogVisible = false, + ) + } + } + + private fun showDeleteDialog() { + _viewState.update { + it.copy( + deleteDialogVisible = true, + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +data class FeedOptionViewState( + var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), + val feed: Feed? = null, + val selectedGroupId: String = "", + val newGroupContent: String = "", + val newGroupSelected: Boolean = false, + val groups: List = emptyList(), + val deleteDialogVisible: Boolean = false, +) + +sealed class FeedOptionViewAction { + data class Show( + val scope: CoroutineScope, + val feedId: String + ) : FeedOptionViewAction() + + data class Hide( + val scope: CoroutineScope, + ) : FeedOptionViewAction() + + object ChangeAllowNotificationPreset : FeedOptionViewAction() + object ChangeParseFullContentPreset : FeedOptionViewAction() + + data class SelectedGroup( + val groupId: String + ) : FeedOptionViewAction() + + data class InputNewGroup( + val content: String + ) : FeedOptionViewAction() + + data class SelectedNewGroup( + val selected: Boolean + ) : FeedOptionViewAction() + + data class Delete( + val callback: () -> Unit = {} + ) : FeedOptionViewAction() + + object ShowDeleteDialog: FeedOptionViewAction() + object HideDeleteDialog: FeedOptionViewAction() +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt index 54ad4dc..d7d5dd9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedItem.kt @@ -1,7 +1,8 @@ package me.ash.reader.ui.page.home.feeds +import android.view.HapticFeedbackConstants import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -9,24 +10,45 @@ import androidx.compose.material3.Badge import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import me.ash.reader.data.feed.Feed +import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction +import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewModel +@OptIn( + androidx.compose.foundation.ExperimentalFoundationApi::class, + androidx.compose.material.ExperimentalMaterialApi::class, +) @Composable fun FeedItem( modifier: Modifier = Modifier, - name: String, - important: Int, + feed: Feed, + feedOptionViewModel: FeedOptionViewModel = hiltViewModel(), onClick: () -> Unit = {}, ) { + val view = LocalView.current + val scope = rememberCoroutineScope() + Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 14.dp) .clip(RoundedCornerShape(32.dp)) - .clickable { onClick() } + .combinedClickable( + onClick = { + onClick() + }, + onLongClick = { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + feedOptionViewModel.dispatch(FeedOptionViewAction.Show(scope, feed.id)) + } + ) .padding(vertical = 14.dp), ) { Row( @@ -43,19 +65,19 @@ fun FeedItem( ) {} Spacer(modifier = Modifier.width(12.dp)) Text( - text = name, + text = feed.name, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } - if (important != 0) { + if (feed.important ?: 0 != 0) { Badge( modifier = Modifier.padding(end = 6.dp), containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f), contentColor = MaterialTheme.colorScheme.outline, content = { Text( - text = important.toString(), + text = feed.important.toString(), style = MaterialTheme.typography.labelSmall ) }, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt index 227f05b..90ee02b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsPage.kt @@ -1,7 +1,9 @@ package me.ash.reader.ui.page.home.feeds +import androidx.compose.animation.Crossfade import androidx.compose.animation.core.* import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -23,7 +25,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import kotlinx.coroutines.flow.collect import me.ash.reader.R import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.extension.getDesc @@ -167,44 +168,48 @@ fun FeedsPage( Spacer(modifier = Modifier.height(8.dp)) } itemsIndexed(viewState.groupWithFeedList) { index, groupWithFeed -> - GroupItem( - text = groupWithFeed.group.name, - feeds = groupWithFeed.feeds, - groupOnClick = { - homeViewModel.dispatch( - HomeViewAction.ChangeFilter( - filterState.copy( - group = groupWithFeed.group, - feed = null + Crossfade(targetState = groupWithFeed) { groupWithFeed -> + Column { + GroupItem( + text = groupWithFeed.group.name, + feeds = groupWithFeed.feeds, + groupOnClick = { + homeViewModel.dispatch( + HomeViewAction.ChangeFilter( + filterState.copy( + group = groupWithFeed.group, + feed = null + ) + ) ) - ) - ) - homeViewModel.dispatch( - HomeViewAction.ScrollToPage( - scope = scope, - targetPage = 1, - ) - ) - }, - feedOnClick = { feed -> - homeViewModel.dispatch( - HomeViewAction.ChangeFilter( - filterState.copy( - group = null, - feed = feed + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + ) ) - ) - ) - homeViewModel.dispatch( - HomeViewAction.ScrollToPage( - scope = scope, - targetPage = 1, - ) + }, + feedOnClick = { feed -> + homeViewModel.dispatch( + HomeViewAction.ChangeFilter( + filterState.copy( + group = null, + feed = feed + ) + ) + ) + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + ) + ) + } ) + if (index != viewState.groupWithFeedList.lastIndex) { + Spacer(modifier = Modifier.height(8.dp)) + } } - ) - if (index != viewState.groupWithFeedList.lastIndex) { - Spacer(modifier = Modifier.height(8.dp)) } } item { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt index 66a1bad..e43a0bf 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/GroupItem.kt @@ -20,10 +20,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch import me.ash.reader.R import me.ash.reader.data.feed.Feed -import me.ash.reader.ui.page.common.LocalDrawerState @OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable @@ -36,8 +34,6 @@ fun GroupItem( feedOnClick: (feed: Feed) -> Unit = {}, ) { var expanded by remember { mutableStateOf(isExpanded) } - val scope = rememberCoroutineScope() - val drawerState = LocalDrawerState.current Column( modifier = Modifier @@ -50,9 +46,6 @@ fun GroupItem( groupOnClick() }, onLongClick = { - scope.launch { - drawerState.show() - } } ) .padding(top = 22.dp) @@ -97,8 +90,7 @@ fun GroupItem( feeds.forEach { feed -> FeedItem( modifier = Modifier.padding(horizontal = 20.dp), - name = feed.name, - important = feed.important ?: 0, + feed = feed, ) { feedOnClick(feed) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt index d01228c..c612d12 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/ResultViewPage.kt @@ -1,24 +1,29 @@ package me.ash.reader.ui.page.home.feeds.subscribe +import android.content.Intent +import android.net.Uri import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AddAlert -import androidx.compose.material.icons.filled.Article +import androidx.compose.material.icons.outlined.Article +import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.MainAxisAlignment import me.ash.reader.R import me.ash.reader.data.group.Group +import me.ash.reader.ui.extension.roundClick import me.ash.reader.ui.widget.SelectionChip import me.ash.reader.ui.widget.SelectionEditorChip import me.ash.reader.ui.widget.Subtitle @@ -74,15 +79,23 @@ fun ResultViewPage( private fun Link( text: String, ) { + val context = LocalContext.current Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { SelectionContainer { Text( + modifier = Modifier.roundClick { + context.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(text)) + ) + }, text = text, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } @@ -108,12 +121,11 @@ private fun Preset( selected = selectedAllowNotificationPreset, selectedIcon = { Icon( - imageVector = Icons.Filled.AddAlert, + imageVector = Icons.Outlined.Notifications, contentDescription = stringResource(R.string.allow_notification), modifier = Modifier .padding(start = 8.dp) .size(20.dp), - tint = MaterialTheme.colorScheme.onSurface, ) }, ) { @@ -125,12 +137,11 @@ private fun Preset( selected = selectedParseFullContentPreset, selectedIcon = { Icon( - imageVector = Icons.Filled.Article, + imageVector = Icons.Outlined.Article, contentDescription = stringResource(R.string.parse_full_content), modifier = Modifier .padding(start = 8.dp) .size(20.dp), - tint = MaterialTheme.colorScheme.onSurface, ) }, ) { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SearchViewPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SearchViewPage.kt index ec55903..99ffa70 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SearchViewPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SearchViewPage.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -46,6 +47,7 @@ fun SearchViewPage( onKeyboardAction: () -> Unit = {}, ) { val focusManager = LocalFocusManager.current + val clipboardManager = LocalClipboardManager.current val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { @@ -89,6 +91,7 @@ fun SearchViewPage( } } else { IconButton(onClick = { + onLinkValueChange(clipboardManager.getText()?.text ?: "") }) { Icon( imageVector = Icons.Rounded.ContentPaste, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt index e12aae6..d5cfba5 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeDialog.kt @@ -18,15 +18,17 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.pager.ExperimentalPagerApi +import kotlinx.coroutines.Dispatchers import me.ash.reader.* import me.ash.reader.R import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.widget.Dialog import java.io.InputStream -@OptIn(ExperimentalPagerApi::class) +@OptIn(ExperimentalPagerApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable fun SubscribeDialog( modifier: Modifier = Modifier, @@ -44,8 +46,9 @@ fun SubscribeDialog( } } val viewState = viewModel.viewState.collectAsStateValue() - val groupsState = viewState.groups.collectAsState(initial = emptyList()) - var dialogHeight by remember { mutableStateOf(280.dp) } + val groupsState = + viewState.groups.collectAsState(initial = emptyList(), context = Dispatchers.IO) + var dialogHeight by remember { mutableStateOf(300.dp) } val readYouString = stringResource(R.string.read_you) val defaultString = stringResource(R.string.defaults) LaunchedEffect(viewState.visible) { @@ -64,7 +67,7 @@ fun SubscribeDialog( LaunchedEffect(viewState.pagerState.currentPage) { focusManager.clearFocus() when (viewState.pagerState.currentPage) { - 0 -> dialogHeight = 280.dp + 0 -> dialogHeight = 300.dp 1 -> dialogHeight = Dp.Unspecified } } @@ -74,6 +77,7 @@ fun SubscribeDialog( .padding(horizontal = 44.dp) .height(dialogHeight), visible = viewState.visible, + properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() viewModel.dispatch(SubscribeViewAction.Hide) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt index 3e7985c..21d3e9c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewModel.kt @@ -187,7 +187,8 @@ class SubscribeViewModel @Inject constructor( private fun inputLink(content: String) { _viewState.update { it.copy( - linkContent = content + linkContent = content, + errorMessage = "", ) } } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index f917581..c8c7251 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -1,6 +1,7 @@ package me.ash.reader.ui.page.home.flow import android.widget.Toast +import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth @@ -79,7 +80,11 @@ fun FlowPage( actions = { IconButton(onClick = { viewModel.dispatch(FlowViewAction.PeekSyncWork) - Toast.makeText(context, viewState.syncWorkInfo.length.toString(), Toast.LENGTH_SHORT) + Toast.makeText( + context, + viewState.syncWorkInfo.length.toString(), + Toast.LENGTH_SHORT + ) .show() }) { Icon( @@ -99,31 +104,33 @@ fun FlowPage( ) }, content = { - LazyColumn( - state = viewState.listState, - ) { - item { - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - start = if (true) 54.dp else 24.dp, - top = 48.dp, - end = 24.dp, - bottom = 24.dp - ), - text = when { - filterState.group != null -> filterState.group.name - filterState.feed != null -> filterState.feed.name - else -> filterState.filter.getName() - }, - style = MaterialTheme.typography.displaySmall, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Crossfade(targetState = pagingItems) { pagingItems -> + LazyColumn( + state = viewState.listState, + ) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = if (true) 54.dp else 24.dp, + top = 48.dp, + end = 24.dp, + bottom = 24.dp + ), + text = when { + filterState.group != null -> filterState.group.name + filterState.feed != null -> filterState.feed.name + else -> filterState.filter.getName() + }, + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope) } - generateArticleList(context, pagingItems, readViewModel, homeViewModel, scope) } } ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt index 3587ac4..24f9b25 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/Header.kt @@ -8,9 +8,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import me.ash.reader.data.article.Article import me.ash.reader.data.feed.Feed import me.ash.reader.formatToString @@ -30,37 +28,31 @@ fun Header( Intent(Intent.ACTION_VIEW, Uri.parse(article.link)) ) } + .padding(12.dp) ) { - Column(modifier = Modifier.padding(10.dp)) { + Text( + text = article.date.formatToString(context, atHourMinute = true), + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.labelMedium, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = article.title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.headlineLarge, + ) + Spacer(modifier = Modifier.height(4.dp)) + article.author?.let { Text( - text = article.date.formatToString(context, atHourMinute = true), - fontSize = 12.sp, - color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.Medium, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = article.title, - fontSize = 27.sp, - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, - lineHeight = 34.sp, - ) - Spacer(modifier = Modifier.height(4.dp)) - article.author?.let { - Text( - text = article.author, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.Medium, - ) - } - Text( - text = feed.name, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.Medium, + text = article.author, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.labelMedium, ) } + Text( + text = feed.name, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.labelMedium, + ) } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt index 096662a..8f23571 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPage.kt @@ -1,34 +1,51 @@ package me.ash.reader.ui.page.home.read -import androidx.compose.animation.* +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Headphones +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.rememberLottieComposition -import kotlinx.coroutines.flow.collect +import me.ash.reader.R import me.ash.reader.ui.extension.collectAsStateValue -import me.ash.reader.ui.extension.paddingFixedHorizontal +import me.ash.reader.ui.page.home.HomeViewAction +import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.widget.WebView +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReadPage( navController: NavHostController, modifier: Modifier = Modifier, viewModel: ReadViewModel = hiltViewModel(), - btnBackOnClickListener: () -> Unit, + homeViewModel: HomeViewModel = hiltViewModel(), + readViewModel: ReadViewModel = hiltViewModel(), ) { val context = LocalContext.current + val scope = rememberCoroutineScope() val viewState = viewModel.viewState.collectAsStateValue() + val composition by rememberLottieComposition( + LottieCompositionSpec.Url( + "https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json" + ) + ) LaunchedEffect(viewModel.viewState) { viewModel.viewState.collect { @@ -43,18 +60,52 @@ fun ReadPage( } } - Box { - Column( - modifier.fillMaxSize() - ) { - ReadPageTopBar(btnBackOnClickListener) - - val composition by rememberLottieComposition( - LottieCompositionSpec.Url( - "https://assets5.lottiefiles.com/packages/lf20_9tvcldy3.json" - ) + Scaffold( + modifier = Modifier.background(MaterialTheme.colorScheme.surface), + topBar = { + SmallTopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = { + homeViewModel.dispatch( + HomeViewAction.ScrollToPage( + scope = scope, + targetPage = 1, + callback = { + readViewModel.dispatch(ReadViewAction.ClearArticle) + } + ) + ) + }) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, + actions = { + viewState.articleWithFeed?.let { + IconButton(onClick = {}) { + Icon( + modifier = Modifier.size(22.dp), + imageVector = Icons.Outlined.Headphones, + contentDescription = stringResource(R.string.mark_all_as_read), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.search), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + } ) - + }, + content = { if (viewState.articleWithFeed == null) { LottieAnimation( composition = composition, @@ -65,41 +116,34 @@ fun ReadPage( restartOnPlay = true, iterations = Int.MAX_VALUE ) - } - AnimatedVisibility( - modifier = modifier.fillMaxSize(), - visible = viewState.articleWithFeed != null, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - if (viewState.articleWithFeed == null) return@AnimatedVisibility + } else { LazyColumn( state = viewState.listState, - modifier = Modifier - .weight(1f), ) { val article = viewState.articleWithFeed.article val feed = viewState.articleWithFeed.feed item { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(2.dp)) Column( modifier = Modifier - .weight(1f) - .paddingFixedHorizontal() + .padding(horizontal = 12.dp) ) { Header(context, article, feed) } } + item { - Spacer(modifier = Modifier.height(40.dp)) - WebView( - content = viewState.content ?: "", - ) - Spacer(modifier = Modifier.height(50.dp)) + Spacer(modifier = Modifier.height(22.dp)) + Crossfade(targetState = viewState.content) { content -> + WebView( + content = content ?: "", + ) + Spacer(modifier = Modifier.height(50.dp)) + } } } } } - } + ) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt deleted file mode 100644 index 6753012..0000000 --- a/app/src/main/java/me/ash/reader/ui/page/home/read/ReadPageTopBar.kt +++ /dev/null @@ -1,63 +0,0 @@ -package me.ash.reader.ui.page.home.read - -import android.view.HapticFeedbackConstants -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.MoreHoriz -import androidx.compose.material.icons.rounded.Share -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SmallTopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import me.ash.reader.R - -@Composable -fun ReadPageTopBar( - btnBackOnClickListener: () -> Unit = {}, -) { - val view = LocalView.current - SmallTopAppBar( - title = {}, - navigationIcon = { - IconButton(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - btnBackOnClickListener() - }) { - Icon( - modifier = Modifier.size(28.dp), - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.primary - ) - } - }, - actions = { - IconButton(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - }) { - Icon( -// modifier = Modifier.size(20.dp), - imageVector = Icons.Rounded.Share, - contentDescription = "Share", - tint = MaterialTheme.colorScheme.primary, - ) - } - IconButton(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - }) { - Icon( -// modifier = Modifier.size(28.dp), - imageVector = Icons.Rounded.MoreHoriz, - contentDescription = "More", - tint = MaterialTheme.colorScheme.primary, - ) - } - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/widget/Banner.kt b/app/src/main/java/me/ash/reader/ui/widget/Banner.kt index b94b009..ef3e81f 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/Banner.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/Banner.kt @@ -1,5 +1,6 @@ package me.ash.reader.ui.widget +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -42,13 +43,15 @@ fun Banner( .padding(16.dp, 20.dp), verticalAlignment = Alignment.CenterVertically ) { - icon?.let { - Icon( - imageVector = it, - contentDescription = null, - modifier = Modifier.padding(end = 16.dp), - tint = lightOnSurface, - ) + icon?.let { icon -> + Crossfade(targetState = icon) { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + tint = lightOnSurface, + ) + } } Column(modifier = Modifier.weight(1f)) { Text( diff --git a/app/src/main/java/me/ash/reader/ui/widget/BottomDrawer.kt b/app/src/main/java/me/ash/reader/ui/widget/BottomDrawer.kt index 7c72817..79317de 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/BottomDrawer.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/BottomDrawer.kt @@ -8,11 +8,10 @@ import androidx.compose.material.ModalBottomSheetDefaults import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -23,51 +22,53 @@ fun BottomDrawer( drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState( ModalBottomSheetValue.Hidden ), + sheetContent: @Composable ColumnScope.() -> Unit = {}, content: @Composable () -> Unit = {}, ) { androidx.compose.material.ModalBottomSheetLayout( + modifier = modifier, + sheetShape = RoundedCornerShape( + topStart = 28.0.dp, + topEnd = 28.0.dp, + bottomEnd = 0.0.dp, + bottomStart = 0.0.dp + ), sheetState = drawerState, - sheetBackgroundColor = Color.Transparent, + sheetBackgroundColor = MaterialTheme.colorScheme.surface, sheetElevation = if (drawerState.isVisible) ModalBottomSheetDefaults.Elevation else 0.dp, sheetContent = { - Row( - modifier = Modifier - .fillMaxWidth() - .clip( - RoundedCornerShape( - topStart = 28.0.dp, - topEnd = 28.0.dp, - bottomEnd = 0.0.dp, - bottomStart = 0.0.dp - ) - ) - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 28.dp) - .navigationBarsPadding() + Surface( + modifier = modifier, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 6.dp, ) { - Box { - Row( - modifier = modifier - .padding(top = 8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { + Row( + modifier = Modifier.padding(horizontal = 28.dp) + ) { + Box { Row( modifier = modifier - .size(38.dp, 4.dp) - .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)) - .zIndex(1f) - ) {} - } - Column { - Spacer(modifier = Modifier.height(40.dp)) - content() - Spacer(modifier = Modifier.height(28.dp)) + .padding(top = 8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = modifier + .size(38.dp, 4.dp) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)) + .zIndex(1f) + ) {} + } + Column { + Spacer(modifier = Modifier.height(40.dp)) + sheetContent() + Spacer(modifier = Modifier.height(28.dp)) + } } } } }, - content = {} + content = content, ) } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/widget/CanBeDisabledIconButton.kt b/app/src/main/java/me/ash/reader/ui/widget/CanBeDisabledIconButton.kt index 6d81d77..9bbb81b 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/CanBeDisabledIconButton.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/CanBeDisabledIconButton.kt @@ -1,5 +1,6 @@ package me.ash.reader.ui.widget +import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor @@ -9,20 +10,23 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp @Composable fun CanBeDisabledIconButton( modifier: Modifier = Modifier, disabled: Boolean, imageVector: ImageVector, + size: Dp = 24.dp, contentDescription: String?, tint: Color = LocalContentColor.current, onClick: () -> Unit = {}, ) { IconButton( - modifier = Modifier.alpha( + modifier = modifier.alpha( if (disabled) { - 0.7f + 0.5f } else { 1f } @@ -31,7 +35,7 @@ fun CanBeDisabledIconButton( onClick = onClick, ) { Icon( - modifier = modifier, + modifier = Modifier.size(size), imageVector = imageVector, contentDescription = contentDescription, tint = if (disabled) MaterialTheme.colorScheme.outline else tint, diff --git a/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt b/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt index 0df306f..11e23e7 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/Dialog.kt @@ -2,15 +2,14 @@ package me.ash.reader.ui.widget import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.window.DialogProperties -@OptIn(ExperimentalComposeUiApi::class) @Composable fun Dialog( modifier: Modifier = Modifier, visible: Boolean, + properties: DialogProperties = DialogProperties(), onDismissRequest: () -> Unit = {}, icon: @Composable (() -> Unit)? = null, title: @Composable (() -> Unit)? = null, @@ -20,7 +19,7 @@ fun Dialog( ) { if (visible) { AlertDialog( - properties = DialogProperties(usePlatformDefaultWidth = false), + properties = properties, modifier = modifier, onDismissRequest = onDismissRequest, icon = icon, diff --git a/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt b/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt index 5f3d04b..0959ccc 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/SelectionChip.kt @@ -2,10 +2,7 @@ package me.ash.reader.ui.widget import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -14,7 +11,7 @@ import androidx.compose.material.ChipDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.FilterChip import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -41,16 +38,7 @@ fun SelectionChip( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = CircleShape, border: BorderStroke? = null, - selectedIcon: @Composable () -> Unit = { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = stringResource(R.string.selected), - modifier = Modifier - .padding(start = 8.dp) - .size(20.dp), - tint = MaterialTheme.colorScheme.onSurface - ) - }, + selectedIcon: @Composable (() -> Unit)? = null, onClick: () -> Unit, ) { val focusManager = LocalFocusManager.current @@ -59,20 +47,28 @@ fun SelectionChip( modifier = modifier, colors = ChipDefaults.filterChipColors( backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.outline, - leadingIconColor = MaterialTheme.colorScheme.onSurface, - disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + leadingIconColor = MaterialTheme.colorScheme.surfaceVariant, + disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer, - selectedContentColor = MaterialTheme.colorScheme.onSurface, - selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface + selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer ), border = border, interactionSource = interactionSource, enabled = enabled, selected = selected, - selectedIcon = selectedIcon, + selectedIcon = selectedIcon ?: { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = stringResource(R.string.selected), + modifier = Modifier + .padding(start = 8.dp) + .size(20.dp), + ) + }, shape = shape, onClick = { focusManager.clearFocus() @@ -88,11 +84,6 @@ fun SelectionChip( ), text = content, style = MaterialTheme.typography.titleSmall, - color = if (selected) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.outline - }, ) }, ) @@ -108,16 +99,7 @@ fun SelectionEditorChip( enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = CircleShape, - selectedIcon: @Composable () -> Unit = { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = stringResource(R.string.selected), - modifier = Modifier - .padding(start = 8.dp) - .size(20.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - }, + selectedIcon: @Composable (() -> Unit)? = null, onKeyboardAction: () -> Unit = {}, onClick: () -> Unit, ) { @@ -125,21 +107,30 @@ fun SelectionEditorChip( val placeholder = stringResource(R.string.add_to_group) FilterChip( + modifier = modifier.defaultMinSize(minHeight = 36.dp), colors = ChipDefaults.filterChipColors( backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - leadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer, - disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), - disabledLeadingIconColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + leadingIconColor = MaterialTheme.colorScheme.surfaceVariant, + disabledBackgroundColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f), + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), selectedBackgroundColor = MaterialTheme.colorScheme.primaryContainer, - selectedContentColor = MaterialTheme.colorScheme.onSecondaryContainer, - selectedLeadingIconColor = MaterialTheme.colorScheme.onSecondaryContainer + selectedContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + selectedLeadingIconColor = MaterialTheme.colorScheme.onPrimaryContainer ), interactionSource = interactionSource, enabled = enabled, selected = selected, - selectedIcon = selectedIcon, + selectedIcon = selectedIcon ?: { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = stringResource(R.string.selected), + modifier = Modifier + .padding(start = 8.dp) + .size(20.dp), + ) + }, shape = shape, onClick = onClick, content = { @@ -160,13 +151,9 @@ fun SelectionEditorChip( }, value = content, onValueChange = { onValueChange(it) }, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), textStyle = MaterialTheme.typography.titleSmall.copy( - color = if (selected) { - MaterialTheme.colorScheme.onSurface - } else { - MaterialTheme.colorScheme.outline - }, + color = MaterialTheme.colorScheme.onSurfaceVariant, ), decorationBox = { innerTextField -> Row( @@ -176,7 +163,7 @@ fun SelectionEditorChip( if (content.isEmpty()) { Text( text = placeholder, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), style = MaterialTheme.typography.titleSmall, ) } diff --git a/app/src/main/java/me/ash/reader/ui/widget/WebView.kt b/app/src/main/java/me/ash/reader/ui/widget/WebView.kt index 8ff95f6..4f928f3 100644 --- a/app/src/main/java/me/ash/reader/ui/widget/WebView.kt +++ b/app/src/main/java/me/ash/reader/ui/widget/WebView.kt @@ -17,6 +17,8 @@ import me.ash.reader.ui.extension.collectAsStateValue import me.ash.reader.ui.page.home.read.ReadViewAction import me.ash.reader.ui.page.home.read.ReadViewModel +const val INJECTION_TOKEN = "/android_asset_font/" + @Composable fun WebView( modifier: Modifier = Modifier, @@ -26,11 +28,30 @@ fun WebView( onReceivedError: (error: WebResourceError?) -> Unit = {} ) { val context = LocalContext.current - val color = MaterialTheme.colorScheme.secondary.toArgb() + val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() val backgroundColor = MaterialTheme.colorScheme.surface.toArgb() val viewState = viewModel.viewState.collectAsStateValue() val webViewClient = object : WebViewClient() { + override fun shouldInterceptRequest(view: WebView?, url: String?): WebResourceResponse? { + if (url != null && url.contains(INJECTION_TOKEN)) { + try { + val assetPath = url.substring( + url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length, + url.length + ) + return WebResourceResponse( + "text/HTML", + "UTF-8", + context.assets.open(assetPath) + ) + } catch (e: Exception) { + Log.e("RLog", "WebView shouldInterceptRequest: $e") + } + } + return super.shouldInterceptRequest(view, url); + } + override fun onPageStarted( view: WebView?, url: String?, @@ -131,23 +152,29 @@ fun getStyle(argb: Int): String = """ *{ padding: 0; margin: 0; - color: ${argbToCssColor(argb)} + color: ${argbToCssColor(argb)}; + font-family: url('/android_asset_font/font/google_sans_text_regular.TTF'), + url('/android_asset_font/font/google_sans_text_medium_italic.TTF'), + url('/android_asset_font/font/google_sans_text_medium.TTF'), + url('/android_asset_font/font/google_sans_text_italic.TTF'), + url('/android_asset_font/font/google_sans_text_bold_italic.TTF'), + url('/android_asset_font/font/google_sans_text_bold.TTF'); } .page { - padding: 0 20px; + padding: 0 24px; } img { - margin: 0 -20px 20px; - width: calc(100% + 40px); + margin: 0 -24px 20px; + width: calc(100% + 48px); height: auto; } p,span,a,ol,ul,blockquote,article,section { - text-align: justify; - font-size: 18px; - line-height: 32px; + text-align: left; + font-size: 16px; + line-height: 24px; margin-bottom: 20px; } @@ -186,6 +213,7 @@ hr { } h1,h2,h3,h4,h5,h6,figure,br { + font-size: large; margin-bottom: 20px; } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 039940d..79754ec 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -30,6 +30,12 @@ 全文解析 添加到组 新建分组 + 打开 %1$s + 选项 + 删除 + %1$s 已被删除 + 取消订阅 + 不再订阅 %1$s,同时删除其所有已归档的文章。 今天 昨天 %1$s %2$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5c0279..bfedd6a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,12 @@ Parse Full Content Add to Group New Group + Open %1$s + Options + Delete + %1$s has been deleted + Unsubscribe + Unsubscribe %1$s and delete all its archived articles. Today Yesterday %1$s At %2$s