From b2fe0674c85d97610c09be8b002fb68b38650db6 Mon Sep 17 00:00:00 2001 From: Ash Date: Wed, 23 Mar 2022 17:28:53 +0800 Subject: [PATCH] Refactor Material You design for FilterBar and Add subscribe feature --- app/src/main/java/me/ash/reader/App.kt | 20 +- .../me/ash/reader/data/article/ArticleDao.kt | 2 +- .../me/ash/reader/data/constant/Filter.kt | 6 + .../java/me/ash/reader/data/feed/FeedDao.kt | 2 +- .../data/repository/AbstractRssRepository.kt | 27 ++- .../data/repository/FeverRssRepository.kt | 13 + .../data/repository/LocalRssRepository.kt | 18 +- .../reader/data/repository/OpmlRepository.kt | 12 +- .../reader/data/source/OpmlLocalDataSource.kt | 4 +- .../ash/reader/ui/extension/PagerStateExt.kt | 6 +- .../reader/ui/page/home/HomeBottomNavBar.kt | 225 ++++++++++++------ .../me/ash/reader/ui/page/home/HomePage.kt | 2 +- .../ash/reader/ui/page/home/HomeViewModel.kt | 4 +- .../reader/ui/page/home/feeds/FeedsPage.kt | 7 +- .../ui/page/home/feeds/FeedsViewModel.kt | 7 +- .../reader/ui/page/home/feeds/GroupItem.kt | 8 +- .../home/feeds/subscribe/ResultViewPage.kt | 39 ++- .../home/feeds/subscribe/SearchViewPage.kt | 35 ++- .../home/feeds/subscribe/SubscribeDialog.kt | 58 +++-- .../feeds/subscribe/SubscribeViewModel.kt | 111 ++++++--- .../feeds/subscribe/SubscribeViewPager.kt | 35 ++- .../reader/ui/page/home/flow/ArticleItem.kt | 6 +- .../reader/ui/page/home/flow/FlowViewModel.kt | 3 +- .../main/java/me/ash/reader/ui/theme/Theme.kt | 38 ++- .../java/me/ash/reader/ui/widget/Banner.kt | 23 +- .../java/me/ash/reader/ui/widget/Dialog.kt | 12 +- .../me/ash/reader/ui/widget/SelectionChip.kt | 85 +++++-- 27 files changed, 562 insertions(+), 246 deletions(-) diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt index a433564..fe16943 100644 --- a/app/src/main/java/me/ash/reader/App.kt +++ b/app/src/main/java/me/ash/reader/App.kt @@ -2,7 +2,8 @@ package me.ash.reader import android.app.Application import androidx.hilt.work.HiltWorkerFactory -import androidx.work.* +import androidx.work.Configuration +import androidx.work.WorkManager import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -12,7 +13,6 @@ import me.ash.reader.data.repository.* import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.ReaderDatabase import me.ash.reader.data.source.RssNetworkDataSource -import java.util.concurrent.TimeUnit import javax.inject.Inject @DelicateCoroutinesApi @@ -54,7 +54,7 @@ class App : Application(), Configuration.Provider { @Inject lateinit var rssRepository: RssRepository - private val applicationScope = CoroutineScope(Dispatchers.IO) + private val applicationScope = CoroutineScope(Dispatchers.Default) override fun onCreate() { super.onCreate() @@ -73,19 +73,7 @@ class App : Application(), Configuration.Provider { } private fun workerInit() { - val repeatingRequest = PeriodicWorkRequestBuilder( - 15, TimeUnit.MINUTES - ).setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - ).addTag(SyncWorker.WORK_NAME).build() - - workManager.enqueueUniquePeriodicWork( - SyncWorker.WORK_NAME, - ExistingPeriodicWorkPolicy.REPLACE, - repeatingRequest - ) + rssRepository.get().doSync() } override fun getWorkManagerConfiguration(): Configuration = 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 d6649f3..236c435 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 @@ -279,7 +279,7 @@ interface ArticleDao { WHERE id = :id """ ) - suspend fun queryById(id: Int): ArticleWithFeed? + suspend fun queryById(id: String): ArticleWithFeed? @Insert suspend fun insert(article: Article): Long diff --git a/app/src/main/java/me/ash/reader/data/constant/Filter.kt b/app/src/main/java/me/ash/reader/data/constant/Filter.kt index 709e77c..9d70590 100644 --- a/app/src/main/java/me/ash/reader/data/constant/Filter.kt +++ b/app/src/main/java/me/ash/reader/data/constant/Filter.kt @@ -1,7 +1,9 @@ package me.ash.reader.data.constant import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FiberManualRecord import androidx.compose.material.icons.outlined.FiberManualRecord +import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.Subject import androidx.compose.ui.graphics.vector.ImageVector @@ -10,6 +12,7 @@ class Filter( var index: Int, var important: Int, var icon: ImageVector, + var filledIcon: ImageVector, ) { fun isStarred(): Boolean = this == Starred fun isUnread(): Boolean = this == Unread @@ -20,16 +23,19 @@ class Filter( index = 0, important = 13, icon = Icons.Rounded.StarOutline, + filledIcon = Icons.Rounded.Star, ) val Unread = Filter( index = 1, important = 666, icon = Icons.Outlined.FiberManualRecord, + filledIcon = Icons.Filled.FiberManualRecord, ) val All = Filter( index = 2, important = 666, icon = Icons.Rounded.Subject, + filledIcon = Icons.Rounded.Subject, ) } } \ No newline at end of file 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 abba6d0..7dce150 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 @@ -19,7 +19,7 @@ interface FeedDao { and url = :url """ ) - fun queryByLink(accountId: Int, url: String): List + suspend fun queryByLink(accountId: Int, url: String): List @Insert suspend fun insert(feed: Feed): Long 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 0149284..cf1ed72 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 @@ -4,9 +4,7 @@ import android.content.Context import android.util.Log import androidx.hilt.work.HiltWorker import androidx.paging.PagingSource -import androidx.work.CoroutineWorker -import androidx.work.WorkManager -import androidx.work.WorkerParameters +import androidx.work.* import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow @@ -28,6 +26,7 @@ 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( private val context: Context, @@ -51,8 +50,18 @@ abstract class AbstractRssRepository constructor( abstract suspend fun subscribe(feed: Feed, articles: List
) + abstract suspend fun addGroup(name: String): String + abstract suspend fun sync() + fun doSync() { + workManager.enqueueUniquePeriodicWork( + SyncWorker.WORK_NAME, + ExistingPeriodicWorkPolicy.REPLACE, + SyncWorker.repeatingRequest + ) + } + fun pullGroups(): Flow> { val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 return groupDao.queryAllGroup(accountId) @@ -118,11 +127,11 @@ abstract class AbstractRssRepository constructor( } } - suspend fun findArticleById(id: Int): ArticleWithFeed? { + suspend fun findArticleById(id: String): ArticleWithFeed? { return articleDao.queryById(id) } - fun isExist(url: String): Boolean { + suspend fun isExist(url: String): Boolean { val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId)!! return feedDao.queryByLink(accountId, url).isNotEmpty() } @@ -158,5 +167,13 @@ class SyncWorker @AssistedInject constructor( companion object { const val WORK_NAME = "article.sync" + + val repeatingRequest = PeriodicWorkRequestBuilder( + 15, TimeUnit.MINUTES + ).setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + ).addTag(WORK_NAME).build() } } \ No newline at end of file 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 ff7745d..e749965 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 @@ -48,6 +48,19 @@ 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 + ) + ) + } + } + override suspend fun sync() { mutex.withLock { val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) 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 a9d420e..cebf6e0 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 @@ -20,6 +20,7 @@ import me.ash.reader.data.article.Article import me.ash.reader.data.article.ArticleDao import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.FeedDao +import me.ash.reader.data.group.Group import me.ash.reader.data.group.GroupDao import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.ui.page.common.ExtraName @@ -35,7 +36,7 @@ class LocalRssRepository @Inject constructor( private val rssHelper: RssHelper, private val rssNetworkDataSource: RssNetworkDataSource, private val accountDao: AccountDao, - groupDao: GroupDao, + private val groupDao: GroupDao, workManager: WorkManager, ) : AbstractRssRepository( context, accountDao, articleDao, groupDao, @@ -52,6 +53,19 @@ 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 + ) + ) + } + } + override suspend fun sync() { mutex.withLock { val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) @@ -136,7 +150,7 @@ class LocalRssRepository @Inject constructor( Intent.FLAG_ACTIVITY_CLEAR_TASK putExtra( ExtraName.ARTICLE_ID, - ids[index].toInt() + ids[index] ) }, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT diff --git a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt index 9934295..68ce39b 100644 --- a/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt +++ b/app/src/main/java/me/ash/reader/data/repository/OpmlRepository.kt @@ -1,6 +1,7 @@ package me.ash.reader.data.repository import android.util.Log +import me.ash.reader.data.feed.Feed import me.ash.reader.data.feed.FeedDao import me.ash.reader.data.group.GroupDao import me.ash.reader.data.source.OpmlLocalDataSource @@ -18,11 +19,14 @@ class OpmlRepository @Inject constructor( val groupWithFeedList = opmlLocalDataSource.parseFileInputStream(inputStream) groupWithFeedList.forEach { groupWithFeed -> groupDao.insert(groupWithFeed.group) - groupWithFeed.feeds.forEach { it.groupId = groupWithFeed.group.id } - groupWithFeed.feeds.removeIf { - rssRepository.get().isExist(it.url) + val repeatList = mutableListOf() + groupWithFeed.feeds.forEach { + it.groupId = groupWithFeed.group.id + if (rssRepository.get().isExist(it.url)) { + repeatList.add(it) + } } - feedDao.insertList(groupWithFeed.feeds) + feedDao.insertList((groupWithFeed.feeds subtract repeatList).toList()) } } catch (e: Exception) { Log.e("saveToDatabase", "${e.message}") 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 1e89b4b..c1de1d3 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 @@ -11,8 +11,6 @@ import me.ash.reader.data.group.GroupWithFeed import me.ash.reader.dataStore import me.ash.reader.get import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException -import java.io.IOException import java.io.InputStream import java.util.* import javax.inject.Inject @@ -21,7 +19,7 @@ class OpmlLocalDataSource @Inject constructor( @ApplicationContext private val context: Context, ) { - @Throws(XmlPullParserException::class, IOException::class) +// @Throws(XmlPullParserException::class, IOException::class) fun parseFileInputStream(inputStream: InputStream): List { val groupWithFeedList = mutableListOf() val accountId = context.dataStore.get(DataStoreKeys.CurrentAccountId) ?: 0 diff --git a/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt b/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt index 5d9b4fb..080ce47 100644 --- a/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt +++ b/app/src/main/java/me/ash/reader/ui/extension/PagerStateExt.kt @@ -12,7 +12,9 @@ fun PagerState.animateScrollToPage( callback: () -> Unit = {} ) { scope.launch { - animateScrollToPage(targetPage) - callback() + if (pageCount > targetPage) { + animateScrollToPage(targetPage) + callback() + } } } \ No newline at end of file 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 f36ebae..ef71d12 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 @@ -1,6 +1,8 @@ package me.ash.reader.ui.page.home +import android.util.Log import android.view.HapticFeedbackConstants +import android.view.SoundEffectConstants import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.FastOutLinearInEasing @@ -10,9 +12,10 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.clickable 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.FilterChip import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.outlined.Circle @@ -26,13 +29,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import com.google.accompanist.flowlayout.FlowCrossAxisAlignment +import com.google.accompanist.flowlayout.FlowRow +import com.google.accompanist.flowlayout.MainAxisAlignment +import com.google.accompanist.flowlayout.SizeMode import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState import me.ash.reader.R @@ -89,7 +93,7 @@ fun HomeBottomNavBar( } Divider( - modifier = Modifier.alpha(0.3f), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f) ) Box( modifier = modifier @@ -107,7 +111,7 @@ fun HomeBottomNavBar( .animateContentSize() .alpha(1 - readerBarAlpha), ) { -// Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}") + Log.i("RLog", "AppNavigationBar: ${readerBarAlpha}, ${1f - readerBarAlpha}") FilterBar( modifier = modifier, filter = filter, @@ -148,87 +152,154 @@ private fun FilterBar( filter: Filter, onSelected: (Filter) -> Unit = {}, ) { - val view = LocalView.current + Row( modifier = modifier, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - listOf( - Filter.Starred, - Filter.Unread, - Filter.All - ).forEach { item -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .clip(CircleShape) - .animateContentSize(), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .height(30.dp) - .defaultMinSize( - minWidth = 82.dp - ) - .clip(CircleShape) - .clickable(onClick = { - view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) - onSelected(item) - }) - .background( - if (filter == item) { - MaterialTheme.colorScheme.inverseOnSurface - } else { - Color.Unspecified - } - ) + FlowRow( + mainAxisSize = SizeMode.Expand, + mainAxisAlignment = MainAxisAlignment.Center, + crossAxisAlignment = FlowCrossAxisAlignment.Center, + crossAxisSpacing = 0.dp, + mainAxisSpacing = 20.dp, + ) { + listOf( + Filter.Starred, + Filter.Unread, + Filter.All + ).forEach { item -> + Item( + icon = if (filter == item) item.filledIcon else item.icon, + name = item.getName(), + selected = filter == item, ) { - if (filter == item) { - Spacer(modifier = Modifier.width(10.dp)) - Icon( - modifier = Modifier.size( - if (filter == item) { - 15.dp - } else { - 19.dp - } - ), - imageVector = item.icon, - contentDescription = item.getName(), - tint = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = item.getName().uppercase(), - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(modifier = Modifier.width(10.dp)) - } else { - Icon( - modifier = Modifier.size( - if (item.isUnread()) { - 15 - } else { - 19 - }.dp - ), - imageVector = item.icon, - contentDescription = item.getName(), - tint = MaterialTheme.colorScheme.outline, - ) - } + onSelected(item) } } } } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun Item( + modifier: Modifier = Modifier, + icon: ImageVector, + name: String, + selected: Boolean = false, + onClick: () -> Unit = {}, +) { + val view = LocalView.current + + FilterChip( + modifier = Modifier + .height(36.dp) + .animateContentSize(), + colors = ChipDefaults.filterChipColors( + backgroundColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.outline, + leadingIconColor = MaterialTheme.colorScheme.outline, + 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 = MaterialTheme.colorScheme.primaryContainer, + selectedContentColor = MaterialTheme.colorScheme.onSurface, + selectedLeadingIconColor = MaterialTheme.colorScheme.onSurface + ), + selected = selected, + selectedIcon = { + Icon( + imageVector = icon, + contentDescription = name, + modifier = Modifier + .padding(start = 8.dp) + .size(20.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + onClick() + }, + content = { + if (selected) { + Text( + modifier = modifier.padding( + start = 0.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + ), + text = if (selected) name.uppercase() else "", + style = MaterialTheme.typography.titleSmall, + color = if (selected) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.outline + }, + ) + } else { + Icon( + imageVector = icon, + contentDescription = name, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.outline, + ) + } + }, + ) + +// Row( +// modifier = Modifier +// .animateContentSize() +// .height(40.dp) +// .width(if (selected) Dp.Unspecified else 40.dp) +// .padding(vertical = if (selected) 2.dp else 0.dp) +// .clip(CircleShape) +// .pointerInput(Unit) { +// detectTapGestures( +// onTap = { +// view.playSoundEffect(SoundEffectConstants.CLICK) +// onClick() +// } +// ) +// } +// .background( +// if (selected) { +// MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.54f) +// } else { +// Color.Transparent +// } +// ), +// horizontalArrangement = Arrangement.Center, +// verticalAlignment = Alignment.CenterVertically, +// ) { +// Spacer(modifier = Modifier.width(8.dp)) +// Icon( +// modifier = Modifier.size(20.dp), +// imageVector = icon, +// contentDescription = name, +// tint = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, +// ) +// if (selected) { +// Spacer(modifier = Modifier.width(8.dp)) +// Text( +// modifier = Modifier.padding(horizontal = 8.dp), +// text = name.uppercase(), +// style = MaterialTheme.typography.titleSmall, +// color = if (selected) { +// MaterialTheme.colorScheme.onSurface +// } else { +// MaterialTheme.colorScheme.outline +// }, +// ) +// Spacer(modifier = Modifier.width(8.dp)) +// } +// } +} + @Composable private fun ReaderBar( modifier: Modifier = Modifier, 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 007bec8..d13d624 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 @@ -47,7 +47,7 @@ fun HomePage( scope.launch { val article = readViewModel .rssRepository.get() - .findArticleById(it.toString().toInt()) ?: return@launch + .findArticleById(it.toString()) ?: return@launch readViewModel.dispatch(ReadViewAction.InitData(article)) if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent) else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index ef7e6e0..50202da 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -47,8 +47,8 @@ class HomeViewModel @Inject constructor( } private fun sync(callback: () -> Unit = {}) { - viewModelScope.launch(Dispatchers.IO) { - rssRepository.get().sync() + viewModelScope.launch { + rssRepository.get().doSync() callback() } } 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 5ee2c3c..227f05b 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 @@ -106,6 +106,7 @@ fun FeedsPage( tint = MaterialTheme.colorScheme.onSurface, ) } + } ) }, @@ -138,7 +139,6 @@ fun FeedsPage( Icon( imageVector = Icons.Outlined.KeyboardArrowRight, contentDescription = stringResource(R.string.go_to), - tint = MaterialTheme.colorScheme.onSurface, ) }, ) { @@ -161,7 +161,7 @@ fun FeedsPage( item { Spacer(modifier = Modifier.height(24.dp)) Subtitle( - modifier = Modifier.padding(start = 28.dp), + modifier = Modifier.padding(start = 26.dp), text = stringResource(R.string.feeds) ) Spacer(modifier = Modifier.height(8.dp)) @@ -207,6 +207,9 @@ fun FeedsPage( Spacer(modifier = Modifier.height(8.dp)) } } + item { + Spacer(modifier = Modifier.height(48.dp)) + } } } ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt index 89c8d07..d538c3f 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/FeedsViewModel.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.ash.reader.data.account.Account @@ -48,14 +47,14 @@ class FeedsViewModel @Inject constructor( } private fun addFromFile(inputStream: InputStream) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { opmlRepository.saveToDatabase(inputStream) - rssRepository.get().sync() + rssRepository.get().doSync() } } private fun fetchData(filterState: FilterState) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { pullFeeds( isStarred = filterState.filter.isStarred(), isUnread = filterState.filter.isUnread(), 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 562bce9..0f7fc1f 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 @@ -35,7 +35,7 @@ fun GroupItem( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp) + .padding(horizontal = 16.dp) .clip(RoundedCornerShape(32.dp)) .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.14f)) .clickable { groupOnClick() } @@ -78,9 +78,6 @@ fun GroupItem( exit = fadeOut() + shrinkVertically(), ) { Column { - if (feeds.isNotEmpty()) { - Spacer(modifier = Modifier.height(16.dp)) - } feeds.forEach { feed -> FeedItem( modifier = Modifier.padding(horizontal = 20.dp), @@ -90,6 +87,9 @@ fun GroupItem( feedOnClick(feed) } } + if (feeds.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + } } } } 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 bcafcdc..2d707d7 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 @@ -6,8 +6,8 @@ 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.outlined.Article -import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.filled.AddAlert +import androidx.compose.material.icons.filled.Article import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -25,18 +25,23 @@ import me.ash.reader.ui.widget.Subtitle @Composable fun ResultViewPage( + modifier: Modifier = Modifier, link: String = "", groups: List = emptyList(), selectedAllowNotificationPreset: Boolean = false, selectedParseFullContentPreset: Boolean = false, selectedGroupId: String = "", + newGroupContent: String = "", + newGroupSelected: Boolean, + onNewGroupValueChange: (String) -> Unit = {}, + changeNewGroupSelected: (Boolean) -> Unit = {}, allowNotificationPresetOnClick: () -> Unit = {}, parseFullContentPresetOnClick: () -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {}, onKeyboardAction: () -> Unit = {}, ) { Column( - modifier = Modifier.verticalScroll(rememberScrollState()) + modifier = modifier.verticalScroll(rememberScrollState()) ) { Link( text = link @@ -54,6 +59,10 @@ fun ResultViewPage( AddToGroup( groups = groups, selectedGroupId = selectedGroupId, + newGroupContent = newGroupContent, + newGroupSelected = newGroupSelected, + onNewGroupValueChange = onNewGroupValueChange, + changeNewGroupSelected = changeNewGroupSelected, groupOnClick = groupOnClick, onKeyboardAction = onKeyboardAction, ) @@ -98,11 +107,12 @@ private fun Preset( selected = selectedAllowNotificationPreset, selectedIcon = { Icon( - imageVector = Icons.Outlined.Notifications, + imageVector = Icons.Filled.AddAlert, contentDescription = stringResource(R.string.allow_notification), modifier = Modifier .padding(start = 8.dp) - .size(18.dp), + .size(20.dp), + tint = MaterialTheme.colorScheme.onSurface, ) }, ) { @@ -114,11 +124,12 @@ private fun Preset( selected = selectedParseFullContentPreset, selectedIcon = { Icon( - imageVector = Icons.Outlined.Article, + imageVector = Icons.Filled.Article, contentDescription = stringResource(R.string.parse_full_content), modifier = Modifier .padding(start = 8.dp) - .size(18.dp), + .size(20.dp), + tint = MaterialTheme.colorScheme.onSurface, ) }, ) { @@ -131,6 +142,10 @@ private fun Preset( private fun AddToGroup( groups: List, selectedGroupId: String, + newGroupContent: String, + newGroupSelected: Boolean, + onNewGroupValueChange: (String) -> Unit = {}, + changeNewGroupSelected: (Boolean) -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {}, onKeyboardAction: () -> Unit = {}, ) { @@ -145,19 +160,21 @@ private fun AddToGroup( SelectionChip( modifier = Modifier.animateContentSize(), content = it.name, - selected = it.id == selectedGroupId, + selected = !newGroupSelected && it.id == selectedGroupId, ) { + changeNewGroupSelected(false) groupOnClick(it.id) } } SelectionEditorChip( modifier = Modifier.animateContentSize(), - content = stringResource(R.string.new_group), - selected = false, + content = newGroupContent, + onValueChange = onNewGroupValueChange, + selected = newGroupSelected, onKeyboardAction = onKeyboardAction, ) { - + changeNewGroupSelected(true) } } } \ No newline at end of file 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 29bbf77..ec55903 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 @@ -1,10 +1,13 @@ package me.ash.reader.ui.page.home.feeds.subscribe +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults @@ -22,7 +25,9 @@ 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.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState @@ -32,12 +37,15 @@ import me.ash.reader.R @OptIn(ExperimentalPagerApi::class) @Composable fun SearchViewPage( + modifier: Modifier = Modifier, pagerState: PagerState, - inputContent: String = "", + readOnly: Boolean = false, + inputLink: String = "", errorMessage: String = "", - onValueChange: (String) -> Unit = {}, + onLinkValueChange: (String) -> Unit = {}, onKeyboardAction: () -> Unit = {}, ) { + val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { @@ -45,7 +53,7 @@ fun SearchViewPage( focusRequester.requestFocus() } - Column { + Column(modifier = modifier) { Spacer(modifier = Modifier.height(10.dp)) TextField( modifier = Modifier.focusRequester(focusRequester), @@ -55,9 +63,10 @@ fun SearchViewPage( textColor = MaterialTheme.colorScheme.onSurface, focusedIndicatorColor = MaterialTheme.colorScheme.primary, ), - value = inputContent, + enabled = !readOnly, + value = inputLink, onValueChange = { - if (pagerState.currentPage == 0) onValueChange(it) + if (!readOnly) onLinkValueChange(it) }, placeholder = { Text( @@ -68,9 +77,9 @@ fun SearchViewPage( isError = errorMessage.isNotEmpty(), singleLine = true, trailingIcon = { - if (inputContent.isNotEmpty()) { + if (inputLink.isNotEmpty()) { IconButton(onClick = { - onValueChange("") + if (!readOnly) onLinkValueChange("") }) { Icon( imageVector = Icons.Rounded.Close, @@ -90,17 +99,23 @@ fun SearchViewPage( } }, keyboardActions = KeyboardActions( - onDone = { + onSearch = { + focusManager.clearFocus() onKeyboardAction() } - ) + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), ) if (errorMessage.isNotEmpty()) { SelectionContainer { Text( + modifier = Modifier + .padding(start = 16.dp) + .horizontalScroll(rememberScrollState()), text = errorMessage, color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(start = 16.dp), maxLines = 1, softWrap = false, ) 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 6ddbbda..e12aae6 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 @@ -2,6 +2,8 @@ package me.ash.reader.ui.page.home.feeds.subscribe import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.RssFeed import androidx.compose.material3.Icon @@ -9,9 +11,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +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.hilt.navigation.compose.hiltViewModel import com.google.accompanist.pager.ExperimentalPagerApi import me.ash.reader.* @@ -23,10 +29,12 @@ import java.io.InputStream @OptIn(ExperimentalPagerApi::class) @Composable fun SubscribeDialog( + modifier: Modifier = Modifier, viewModel: SubscribeViewModel = hiltViewModel(), openInputStreamCallback: (InputStream) -> Unit, ) { val context = LocalContext.current + val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { it?.let { uri -> @@ -37,13 +45,14 @@ fun SubscribeDialog( } val viewState = viewModel.viewState.collectAsStateValue() val groupsState = viewState.groups.collectAsState(initial = emptyList()) - var height by remember { mutableStateOf(0) } - + var dialogHeight by remember { mutableStateOf(280.dp) } + val readYouString = stringResource(R.string.read_you) + val defaultString = stringResource(R.string.defaults) LaunchedEffect(viewState.visible) { if (viewState.visible) { val defaultGroupId = context.dataStore .get(DataStoreKeys.CurrentAccountId)!! - .spacerDollar("0") + .spacerDollar(readYouString + defaultString) viewModel.dispatch(SubscribeViewAction.SelectedGroup(defaultGroupId)) viewModel.dispatch(SubscribeViewAction.Init) } else { @@ -52,9 +61,21 @@ fun SubscribeDialog( } } + LaunchedEffect(viewState.pagerState.currentPage) { + focusManager.clearFocus() + when (viewState.pagerState.currentPage) { + 0 -> dialogHeight = 280.dp + 1 -> dialogHeight = Dp.Unspecified + } + } + Dialog( + modifier = Modifier + .padding(horizontal = 44.dp) + .height(dialogHeight), visible = viewState.visible, onDismissRequest = { + focusManager.clearFocus() viewModel.dispatch(SubscribeViewAction.Hide) }, icon = { @@ -66,30 +87,35 @@ fun SubscribeDialog( title = { Text( when (viewState.pagerState.currentPage) { - 0 -> stringResource(R.string.subscribe) + 0 -> viewState.title else -> viewState.feed?.name ?: stringResource(R.string.unknown) } ) }, text = { SubscribeViewPager( -// height = when (viewState.pagerState.currentPage) { -// 0 -> 84.dp -// else -> Dp.Unspecified -// }, - inputContent = viewState.inputContent, + readOnly = viewState.lockLinkInput, + inputLink = viewState.linkContent, errorMessage = viewState.errorMessage, - onValueChange = { - viewModel.dispatch(SubscribeViewAction.Input(it)) + onLinkValueChange = { + viewModel.dispatch(SubscribeViewAction.InputLink(it)) }, onSearchKeyboardAction = { viewModel.dispatch(SubscribeViewAction.Search(scope)) }, - link = viewState.inputContent, + link = viewState.linkContent, groups = groupsState.value, selectedAllowNotificationPreset = viewState.allowNotificationPreset, selectedParseFullContentPreset = viewState.parseFullContentPreset, selectedGroupId = viewState.selectedGroupId, + newGroupContent = viewState.newGroupContent, + onNewGroupValueChange = { + viewModel.dispatch(SubscribeViewAction.InputNewGroup(it)) + }, + newGroupSelected = viewState.newGroupSelected, + changeNewGroupSelected = { + viewModel.dispatch(SubscribeViewAction.SelectedNewGroup(it)) + }, pagerState = viewState.pagerState, allowNotificationPresetOnClick = { viewModel.dispatch(SubscribeViewAction.ChangeAllowNotificationPreset) @@ -109,14 +135,15 @@ fun SubscribeDialog( when (viewState.pagerState.currentPage) { 0 -> { TextButton( - enabled = viewState.inputContent.isNotEmpty(), + enabled = viewState.linkContent.isNotEmpty(), onClick = { + focusManager.clearFocus() viewModel.dispatch(SubscribeViewAction.Search(scope)) } ) { Text( text = stringResource(R.string.search), - color = if (viewState.inputContent.isNotEmpty()) { + color = if (viewState.linkContent.isNotEmpty()) { Color.Unspecified } else { MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) @@ -127,6 +154,7 @@ fun SubscribeDialog( 1 -> { TextButton( onClick = { + focusManager.clearFocus() viewModel.dispatch(SubscribeViewAction.Subscribe) } ) { @@ -140,6 +168,7 @@ fun SubscribeDialog( 0 -> { TextButton( onClick = { + focusManager.clearFocus() launcher.launch("*/*") viewModel.dispatch(SubscribeViewAction.Hide) } @@ -150,6 +179,7 @@ fun SubscribeDialog( 1 -> { TextButton( onClick = { + 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 fee275f..3e7985c 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 @@ -6,7 +6,8 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.ash.reader.R @@ -29,6 +30,7 @@ class SubscribeViewModel @Inject constructor( ) : ViewModel() { private val _viewState = MutableStateFlow(SubscribeViewState()) val viewState: StateFlow = _viewState.asStateFlow() + private var searchJob: Job? = null fun dispatch(action: SubscribeViewAction) { when (action) { @@ -36,13 +38,15 @@ class SubscribeViewModel @Inject constructor( is SubscribeViewAction.Reset -> reset() is SubscribeViewAction.Show -> changeVisible(true) is SubscribeViewAction.Hide -> changeVisible(false) - is SubscribeViewAction.Input -> inputLink(action.content) + is SubscribeViewAction.InputLink -> inputLink(action.content) is SubscribeViewAction.Search -> search(action.scope) is SubscribeViewAction.ChangeAllowNotificationPreset -> changeAllowNotificationPreset() is SubscribeViewAction.ChangeParseFullContentPreset -> changeParseFullContentPreset() is SubscribeViewAction.SelectedGroup -> selectedGroup(action.groupId) + is SubscribeViewAction.InputNewGroup -> inputNewGroup(action.content) + is SubscribeViewAction.SelectedNewGroup -> selectedNewGroup(action.selected) is SubscribeViewAction.Subscribe -> subscribe() } } @@ -51,24 +55,17 @@ class SubscribeViewModel @Inject constructor( _viewState.update { it.copy( title = stringsRepository.getString(R.string.subscribe), - groups = rssRepository.get().pullGroups() + groups = rssRepository.get().pullGroups(), ) } } private fun reset() { + searchJob?.cancel() + searchJob = null _viewState.update { - it.copy( - visible = false, + SubscribeViewState().copy( title = stringsRepository.getString(R.string.subscribe), - errorMessage = "", - inputContent = "", - feed = null, - articles = emptyList(), - allowNotificationPreset = false, - parseFullContentPreset = false, - selectedGroupId = "", - groups = emptyFlow(), ) } } @@ -76,11 +73,20 @@ class SubscribeViewModel @Inject constructor( private fun subscribe() { val feed = _viewState.value.feed ?: return val articles = _viewState.value.articles - val groupId = _viewState.value.selectedGroupId - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { + val groupId = async { + if ( + _viewState.value.newGroupSelected && + _viewState.value.newGroupContent.isNotBlank() + ) { + rssRepository.get().addGroup(_viewState.value.newGroupContent) + } else { + _viewState.value.selectedGroupId + } + } rssRepository.get().subscribe( feed.copy( - groupId = groupId, + groupId = groupId.await(), isNotification = _viewState.value.allowNotificationPreset, isFullContent = _viewState.value.parseFullContentPreset, ), articles @@ -97,6 +103,14 @@ class SubscribeViewModel @Inject constructor( } } + private fun selectedNewGroup(selected: Boolean) { + _viewState.update { + it.copy( + newGroupSelected = selected, + ) + } + } + private fun changeParseFullContentPreset() { _viewState.update { it.copy( @@ -114,31 +128,40 @@ class SubscribeViewModel @Inject constructor( } private fun search(scope: CoroutineScope) { - _viewState.value.inputContent.formatUrl().let { str -> - if (str != _viewState.value.inputContent) { + searchJob?.cancel() + viewModelScope.launch { + try { _viewState.update { it.copy( - inputContent = str + errorMessage = "", ) } - } - } - _viewState.update { - it.copy( - title = stringsRepository.getString(R.string.searching), - ) - } - viewModelScope.launch(Dispatchers.IO) { - try { - if (rssRepository.get().isExist(_viewState.value.inputContent)) { + _viewState.value.linkContent.formatUrl().let { str -> + if (str != _viewState.value.linkContent) { + _viewState.update { + it.copy( + linkContent = str + ) + } + } + } + _viewState.update { + it.copy( + title = stringsRepository.getString(R.string.searching), + lockLinkInput = true, + ) + } + if (rssRepository.get().isExist(_viewState.value.linkContent)) { _viewState.update { it.copy( + title = stringsRepository.getString(R.string.subscribe), errorMessage = stringsRepository.getString(R.string.already_subscribed), + lockLinkInput = false, ) } return@launch } - val feedWithArticle = rssHelper.searchFeed(_viewState.value.inputContent) + val feedWithArticle = rssHelper.searchFeed(_viewState.value.linkContent) _viewState.update { it.copy( feed = feedWithArticle.feed, @@ -152,16 +175,27 @@ class SubscribeViewModel @Inject constructor( it.copy( title = stringsRepository.getString(R.string.subscribe), errorMessage = e.message ?: stringsRepository.getString(R.string.unknown), + lockLinkInput = false, ) } } + }.also { + searchJob = it } } private fun inputLink(content: String) { _viewState.update { it.copy( - inputContent = content + linkContent = content + ) + } + } + + private fun inputNewGroup(content: String) { + _viewState.update { + it.copy( + newGroupContent = content ) } } @@ -180,12 +214,15 @@ data class SubscribeViewState( val visible: Boolean = false, val title: String = "", val errorMessage: String = "", - val inputContent: String = "", + val linkContent: String = "", + val lockLinkInput: Boolean = false, val feed: Feed? = null, val articles: List
= emptyList(), val allowNotificationPreset: Boolean = false, val parseFullContentPreset: Boolean = false, val selectedGroupId: String = "", + val newGroupContent: String = "", + val newGroupSelected: Boolean = false, val groups: Flow> = emptyFlow(), val pagerState: PagerState = PagerState(), ) @@ -197,7 +234,7 @@ sealed class SubscribeViewAction { object Show : SubscribeViewAction() object Hide : SubscribeViewAction() - data class Input( + data class InputLink( val content: String ) : SubscribeViewAction() @@ -212,5 +249,13 @@ sealed class SubscribeViewAction { val groupId: String ) : SubscribeViewAction() + data class InputNewGroup( + val content: String + ) : SubscribeViewAction() + + data class SelectedNewGroup( + val selected: Boolean + ) : SubscribeViewAction() + object Subscribe : SubscribeViewAction() } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewPager.kt b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewPager.kt index 0da7f18..99195de 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewPager.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/feeds/subscribe/SubscribeViewPager.kt @@ -1,9 +1,10 @@ package me.ash.reader.ui.page.home.feeds.subscribe -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.PagerState import me.ash.reader.data.group.Group @@ -12,33 +13,47 @@ import me.ash.reader.ui.widget.ViewPager @OptIn(ExperimentalPagerApi::class) @Composable fun SubscribeViewPager( - height: Dp = Dp.Unspecified, - inputContent: String = "", + modifier: Modifier = Modifier, + readOnly: Boolean = false, + inputLink: String = "", errorMessage: String = "", - onValueChange: (String) -> Unit = {}, + onLinkValueChange: (String) -> Unit = {}, onSearchKeyboardAction: () -> Unit = {}, link: String = "", groups: List = emptyList(), selectedAllowNotificationPreset: Boolean = false, selectedParseFullContentPreset: Boolean = false, selectedGroupId: String = "", + newGroupContent: String = "", + onNewGroupValueChange: (String) -> Unit = {}, + newGroupSelected: Boolean, + changeNewGroupSelected: (Boolean) -> Unit = {}, pagerState: PagerState = com.google.accompanist.pager.rememberPagerState(), allowNotificationPresetOnClick: () -> Unit = {}, parseFullContentPresetOnClick: () -> Unit = {}, groupOnClick: (groupId: String) -> Unit = {}, onResultKeyboardAction: () -> Unit = {}, ) { + val focusManager = LocalFocusManager.current + ViewPager( - modifier = Modifier.height(height), + modifier = modifier.pointerInput(Unit) { + detectTapGestures( + onTap = { + focusManager.clearFocus() + } + ) + }, state = pagerState, userScrollEnabled = false, composableList = listOf( { SearchViewPage( pagerState = pagerState, - inputContent = inputContent, + readOnly = readOnly, + inputLink = inputLink, errorMessage = errorMessage, - onValueChange = onValueChange, + onLinkValueChange = onLinkValueChange, onKeyboardAction = onSearchKeyboardAction, ) }, @@ -49,6 +64,10 @@ fun SubscribeViewPager( selectedAllowNotificationPreset = selectedAllowNotificationPreset, selectedParseFullContentPreset = selectedParseFullContentPreset, selectedGroupId = selectedGroupId, + newGroupContent = newGroupContent, + onNewGroupValueChange = onNewGroupValueChange, + newGroupSelected = newGroupSelected, + changeNewGroupSelected = changeNewGroupSelected, allowNotificationPresetOnClick = allowNotificationPresetOnClick, parseFullContentPresetOnClick = parseFullContentPresetOnClick, groupOnClick = groupOnClick, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt index 447c7b2..9d5056d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/ArticleItem.kt @@ -27,10 +27,10 @@ fun ArticleItem( val context = LocalContext.current Column( modifier = Modifier - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding(horizontal = 12.dp) .clip(RoundedCornerShape(12.dp)) .clickable { onClick(articleWithFeed) } - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding(horizontal = 12.dp, vertical = 12.dp) .alpha(if (articleWithFeed.article.isUnread) 1f else 0.5f), ) { Row( @@ -72,7 +72,7 @@ fun ArticleItem( ) Text( text = articleWithFeed.article.shortDescription, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), style = MaterialTheme.typography.bodySmall, maxLines = 2, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt index 67e2d3a..d41bf35 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt @@ -8,7 +8,6 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import me.ash.reader.data.article.ArticleWithFeed @@ -41,7 +40,7 @@ class FlowViewModel @Inject constructor( } private fun fetchData(filterState: FilterState) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { rssRepository.get().pullImportant(filterState.filter.isStarred(), true) .collect { importantList -> _viewState.update { diff --git a/app/src/main/java/me/ash/reader/ui/theme/Theme.kt b/app/src/main/java/me/ash/reader/ui/theme/Theme.kt index 9242c37..43582b5 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/Theme.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/Theme.kt @@ -4,8 +4,9 @@ import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext -import me.ash.reader.ui.theme.* private val LightThemeColors = lightColorScheme( @@ -37,6 +38,7 @@ private val LightThemeColors = lightColorScheme( inversePrimary = md_theme_light_inversePrimary, // shadow = md_theme_light_shadow, ) + private val DarkThemeColors = darkColorScheme( primary = md_theme_dark_primary, @@ -68,6 +70,10 @@ private val DarkThemeColors = darkColorScheme( // shadow = md_theme_dark_shadow, ) +val LocalLightThemeColors = staticCompositionLocalOf { LightThemeColors } + +val LocalDarkThemeColors = staticCompositionLocalOf { DarkThemeColors } + @Composable fun AppTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), @@ -75,15 +81,27 @@ fun AppTheme( ) { // Dynamic color is available on Android 12+ val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - val colorScheme = when { - dynamicColor && useDarkTheme -> dynamicDarkColorScheme(LocalContext.current) - dynamicColor && !useDarkTheme -> dynamicLightColorScheme(LocalContext.current) - useDarkTheme -> DarkThemeColors + val light = when { + dynamicColor -> dynamicLightColorScheme(LocalContext.current) else -> LightThemeColors } - MaterialTheme( - colorScheme = colorScheme, - typography = AppTypography, - content = content - ) + val dark = when { + dynamicColor -> dynamicDarkColorScheme(LocalContext.current) + else -> DarkThemeColors + } + val colorScheme = when { + useDarkTheme -> dark + else -> light + } + + CompositionLocalProvider( + LocalLightThemeColors provides light, + LocalDarkThemeColors provides dark, + ) { + MaterialTheme( + colorScheme = colorScheme, + typography = AppTypography, + content = content + ) + } } \ 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 5555fb4..b94b009 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 @@ -4,17 +4,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import me.ash.reader.ui.theme.LocalLightThemeColors @Composable fun Banner( @@ -25,6 +24,10 @@ fun Banner( action: (@Composable () -> Unit)? = null, onClick: () -> Unit = {}, ) { + val lightThemeColors = LocalLightThemeColors.current + val lightPrimaryContainer = lightThemeColors.primaryContainer + val lightOnSurface = lightThemeColors.onSurface + Surface( modifier = modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.surface, @@ -34,7 +37,7 @@ fun Banner( .fillMaxWidth() .padding(horizontal = 16.dp) .clip(RoundedCornerShape(32.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) + .background(lightPrimaryContainer) .clickable { onClick() } .padding(16.dp, 20.dp), verticalAlignment = Alignment.CenterVertically @@ -44,7 +47,7 @@ fun Banner( imageVector = it, contentDescription = null, modifier = Modifier.padding(end = 16.dp), - tint = MaterialTheme.colorScheme.onSurface, + tint = lightOnSurface, ) } Column(modifier = Modifier.weight(1f)) { @@ -52,20 +55,22 @@ fun Banner( text = title, maxLines = if (desc == null) 2 else 1, style = MaterialTheme.typography.titleLarge.copy(fontSize = 20.sp), - color = MaterialTheme.colorScheme.onSurface, + color = lightOnSurface, ) desc?.let { Text( text = it, maxLines = 1, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + color = lightOnSurface.copy(alpha = 0.7f), ) } } action?.let { Box(Modifier.padding(start = 16.dp)) { - it() + CompositionLocalProvider(LocalContentColor provides lightOnSurface) { + it() + } } } } 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 6660e39..0df306f 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,9 +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, onDismissRequest: () -> Unit = {}, icon: @Composable (() -> Unit)? = null, @@ -13,13 +18,10 @@ fun Dialog( confirmButton: @Composable () -> Unit, dismissButton: @Composable (() -> Unit)? = null, ) { -// AnimatedVisibility( -// visible = visible, -// enter = fadeIn() + expandVertically(), -// exit = fadeOut() + shrinkVertically(), -// ) { if (visible) { AlertDialog( + properties = DialogProperties(usePlatformDefaultWidth = false), + modifier = modifier, onDismissRequest = onDismissRequest, icon = icon, title = title, 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 0ba6c4d..825d6d3 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 @@ -1,25 +1,32 @@ package me.ash.reader.ui.widget 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.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions 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.rounded.Check +import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import me.ash.reader.R @@ -34,20 +41,23 @@ fun SelectionChip( shape: Shape = CircleShape, selectedIcon: @Composable () -> Unit = { Icon( - imageVector = Icons.Rounded.Check, + imageVector = Icons.Filled.CheckCircle, contentDescription = stringResource(R.string.selected), modifier = Modifier .padding(start = 8.dp) - .size(18.dp) + .size(20.dp), + tint = MaterialTheme.colorScheme.onSurface ) }, onClick: () -> Unit, ) { + val focusManager = LocalFocusManager.current + FilterChip( modifier = modifier, colors = ChipDefaults.filterChipColors( backgroundColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurface, + contentColor = MaterialTheme.colorScheme.outline, leadingIconColor = MaterialTheme.colorScheme.onSurface, disabledBackgroundColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), disabledContentColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), @@ -61,7 +71,10 @@ fun SelectionChip( selected = selected, selectedIcon = selectedIcon, shape = shape, - onClick = onClick, + onClick = { + focusManager.clearFocus() + onClick() + }, content = { Text( modifier = modifier.padding( @@ -72,6 +85,11 @@ fun SelectionChip( ), text = content, style = MaterialTheme.typography.titleSmall, + color = if (selected) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.outline + }, ) }, ) @@ -80,26 +98,30 @@ fun SelectionChip( @OptIn(ExperimentalMaterialApi::class) @Composable fun SelectionEditorChip( - content: String, - selected: Boolean, modifier: Modifier = Modifier, + content: String, + onValueChange: (String) -> Unit = {}, + selected: Boolean, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = CircleShape, selectedIcon: @Composable () -> Unit = { Icon( - imageVector = Icons.Rounded.Check, + imageVector = Icons.Filled.CheckCircle, contentDescription = stringResource(R.string.selected), modifier = Modifier .padding(start = 8.dp) - .size(16.dp) + .size(20.dp), + tint = MaterialTheme.colorScheme.onSecondaryContainer ) }, onKeyboardAction: () -> Unit = {}, onClick: () -> Unit, ) { + val focusManager = LocalFocusManager.current + val placeholder = stringResource(R.string.add_to_group) + FilterChip( - modifier = modifier, colors = ChipDefaults.filterChipColors( backgroundColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.onSecondaryContainer, @@ -123,21 +145,50 @@ fun SelectionEditorChip( .padding( start = if (selected) 0.dp else 8.dp, top = 8.dp, - end = 8.dp, + end = if (content.isEmpty()) 0.dp else 8.dp, bottom = 8.dp ) - .width(56.dp), + .onFocusChanged { + if (it.isFocused) { + onClick() + } else { + focusManager.clearFocus() + } + }, value = content, - onValueChange = {}, + onValueChange = { onValueChange(it) }, + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSecondaryContainer), textStyle = MaterialTheme.typography.titleSmall.copy( - color = MaterialTheme.colorScheme.onSurface + color = if (selected) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.outline + }, ), - singleLine = true, + decorationBox = { innerTextField -> + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + if (content.isEmpty()) { + Text( + text = placeholder, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.titleSmall, + ) + } + } + innerTextField() + }, keyboardActions = KeyboardActions( onDone = { + focusManager.clearFocus() onKeyboardAction() } - ) + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), ) }, )