From 1ba149368b01498a01feeb47002d6abe026e65f2 Mon Sep 17 00:00:00 2001 From: Ash Date: Tue, 5 Apr 2022 04:53:44 +0800 Subject: [PATCH] Add GroupOptionView --- app/src/main/java/me/ash/reader/App.kt | 4 + .../java/me/ash/reader/data/dao/ArticleDao.kt | 14 ++ .../java/me/ash/reader/data/dao/FeedDao.kt | 35 ++++ .../java/me/ash/reader/data/dao/GroupDao.kt | 4 +- .../data/repository/AbstractRssRepository.kt | 16 +- .../data/repository/AccountRepository.kt | 4 +- .../data/repository/LocalRssRepository.kt | 17 +- .../reader/data/repository/OpmlRepository.kt | 8 +- .../java/me/ash/reader/ui/ext/NumberExt.kt | 4 +- .../me/ash/reader/ui/page/home/HomePage.kt | 2 + .../page/home/drawer/feed/DeleteFeedDialog.kt | 6 +- .../group/AllAllowNotificationDialog.kt | 81 ++++++++ .../drawer/group/AllParseFullContentDialog.kt | 81 ++++++++ .../home/drawer/group/DeleteGroupDialog.kt | 76 +++++++ .../home/drawer/group/GroupOptionDrawer.kt | 155 ++++++++++++++ .../home/drawer/group/GroupOptionViewModel.kt | 189 ++++++++++++++++++ .../reader/ui/page/home/feeds/FeedsPage.kt | 2 +- .../reader/ui/page/home/feeds/GroupItem.kt | 15 +- app/src/main/res/values-zh-rCN/strings.xml | 15 +- app/src/main/res/values/strings.xml | 15 +- 20 files changed, 714 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllAllowNotificationDialog.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllParseFullContentDialog.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/drawer/group/DeleteGroupDialog.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionDrawer.kt create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionViewModel.kt diff --git a/app/src/main/java/me/ash/reader/App.kt b/app/src/main/java/me/ash/reader/App.kt index ff34591..79c6e3f 100644 --- a/app/src/main/java/me/ash/reader/App.kt +++ b/app/src/main/java/me/ash/reader/App.kt @@ -69,6 +69,7 @@ class App : Application(), Configuration.Provider { super.onCreate() applicationScope.launch(dispatcherDefault) { accountInit() + dataStoreInit() workerInit() } } @@ -81,6 +82,9 @@ class App : Application(), Configuration.Provider { } } + private fun dataStoreInit() { + } + private fun workerInit() { rssRepository.get().doSync() } diff --git a/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt b/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt index 92641a8..571f9e7 100644 --- a/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt +++ b/app/src/main/java/me/ash/reader/data/dao/ArticleDao.kt @@ -56,6 +56,20 @@ interface ArticleDao { ) suspend fun deleteByFeedId(accountId: Int, feedId: String) + @Query( + """ + DELETE FROM article + WHERE id IN ( + SELECT a.id FROM article AS a, feed AS b, `group` AS c + WHERE a.accountId = :accountId + AND a.feedId = b.id + AND b.groupId = c.id + AND c.id = :groupId + ) + """ + ) + suspend fun deleteByGroupId(accountId: Int, groupId: String) + @Transaction @Query( """ diff --git a/app/src/main/java/me/ash/reader/data/dao/FeedDao.kt b/app/src/main/java/me/ash/reader/data/dao/FeedDao.kt index cbe4c05..80396a3 100644 --- a/app/src/main/java/me/ash/reader/data/dao/FeedDao.kt +++ b/app/src/main/java/me/ash/reader/data/dao/FeedDao.kt @@ -5,6 +5,41 @@ import me.ash.reader.data.entity.Feed @Dao interface FeedDao { + @Query( + """ + UPDATE feed SET isFullContent = :isFullContent + WHERE accountId = :accountId + AND groupId = :groupId + """ + ) + suspend fun updateIsFullContentByGroupId( + accountId: Int, + groupId: String, + isFullContent: Boolean + ) + + @Query( + """ + UPDATE feed SET isNotification = :isNotification + WHERE accountId = :accountId + AND groupId = :groupId + """ + ) + suspend fun updateIsNotificationByGroupId( + accountId: Int, + groupId: String, + isNotification: Boolean + ) + + @Query( + """ + DELETE FROM feed + WHERE groupId = :groupId + AND accountId = :accountId + """ + ) + suspend fun deleteByGroupId(accountId: Int, groupId: String) + @Query( """ SELECT * FROM feed diff --git a/app/src/main/java/me/ash/reader/data/dao/GroupDao.kt b/app/src/main/java/me/ash/reader/data/dao/GroupDao.kt index 3936ebc..40927e1 100644 --- a/app/src/main/java/me/ash/reader/data/dao/GroupDao.kt +++ b/app/src/main/java/me/ash/reader/data/dao/GroupDao.kt @@ -13,7 +13,7 @@ interface GroupDao { WHERE id = :id """ ) - fun queryById(id: String): Group? + suspend fun queryById(id: String): Group? @Transaction @Query( @@ -31,7 +31,7 @@ interface GroupDao { WHERE accountId = :accountId """ ) - fun queryAllGroupWithFeed(accountId: Int): List + suspend fun queryAllGroupWithFeed(accountId: Int): List @Query( """ 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 589d434..d568c54 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 @@ -112,6 +112,10 @@ abstract class AbstractRssRepository constructor( return feedDao.queryById(id) } + suspend fun findGroupById(id: String): Group? { + return groupDao.queryById(id) + } + suspend fun findArticleById(id: String): ArticleWithFeed? { return articleDao.queryById(id) } @@ -133,13 +137,23 @@ abstract class AbstractRssRepository constructor( } suspend fun deleteGroup(group: Group) { - groupDao.update(group) + articleDao.deleteByGroupId(context.currentAccountId, group.id) + feedDao.deleteByGroupId(context.currentAccountId, group.id) + groupDao.delete(group) } suspend fun deleteFeed(feed: Feed) { articleDao.deleteByFeedId(context.currentAccountId, feed.id) feedDao.delete(feed) } + + suspend fun groupParseFullContent(group: Group, isFullContent: Boolean) { + feedDao.updateIsFullContentByGroupId(context.currentAccountId, group.id, isFullContent) + } + + suspend fun groupAllowNotification(group: Group, isNotification: Boolean) { + feedDao.updateIsNotificationByGroupId(context.currentAccountId, group.id, isNotification) + } } @HiltWorker 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 c4f3871..43c45ce 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 @@ -8,7 +8,7 @@ import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.entity.Account import me.ash.reader.data.entity.Group import me.ash.reader.ui.ext.currentAccountId -import me.ash.reader.ui.ext.spacerDollar +import me.ash.reader.ui.ext.getDefaultGroupId import javax.inject.Inject class AccountRepository @Inject constructor( @@ -38,7 +38,7 @@ class AccountRepository @Inject constructor( if (groupDao.queryAll(it.id!!).isEmpty()) { groupDao.insert( Group( - id = it.id!!.spacerDollar(readYouString + defaultString), + id = it.id!!.getDefaultGroupId(), name = defaultString, accountId = it.id!!, ) 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 de8967e..a1ffb91 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 @@ -31,6 +31,7 @@ import me.ash.reader.data.module.DispatcherIO import me.ash.reader.data.repository.SyncWorker.Companion.setIsSyncing import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.ui.ext.currentAccountId +import me.ash.reader.ui.ext.spacerDollar import me.ash.reader.ui.page.common.ExtraName import me.ash.reader.ui.page.common.NotificationGroupName import java.util.* @@ -81,14 +82,16 @@ class LocalRssRepository @Inject constructor( } override suspend fun addGroup(name: String): String { - return UUID.randomUUID().toString().also { - groupDao.insert( - Group( - id = it, - name = name, - accountId = context.currentAccountId + context.currentAccountId.let { accountId -> + return accountId.spacerDollar(UUID.randomUUID().toString()).also { + groupDao.insert( + Group( + id = it, + name = name, + accountId = accountId + ) ) - ) + } } } 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 33ee8c2..304bed4 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 @@ -7,14 +7,13 @@ import be.ceau.opml.entity.Head import be.ceau.opml.entity.Opml import be.ceau.opml.entity.Outline import dagger.hilt.android.qualifiers.ApplicationContext -import me.ash.reader.R import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.entity.Feed import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.ui.ext.currentAccountId -import me.ash.reader.ui.ext.spacerDollar +import me.ash.reader.ui.ext.getDefaultGroupId import java.io.InputStream import java.util.* import javax.inject.Inject @@ -27,7 +26,6 @@ class OpmlRepository @Inject constructor( private val accountDao: AccountDao, private val rssRepository: RssRepository, private val opmlLocalDataSource: OpmlLocalDataSource, - private val stringsRepository: StringsRepository, ) { @Throws(Exception::class) suspend fun saveToDatabase(inputStream: InputStream) { @@ -88,8 +86,6 @@ class OpmlRepository @Inject constructor( } private fun getDefaultGroupId(): String { - val readYouString = stringsRepository.getString(R.string.read_you) - val defaultString = stringsRepository.getString(R.string.defaults) - return context.currentAccountId.spacerDollar(readYouString + defaultString) + return context.currentAccountId.getDefaultGroupId() } } \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/ext/NumberExt.kt b/app/src/main/java/me/ash/reader/ui/ext/NumberExt.kt index 98fdff5..91e0694 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/NumberExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/NumberExt.kt @@ -1,3 +1,5 @@ package me.ash.reader.ui.ext -fun Int.spacerDollar(str: Any): String = "$this$$str" \ No newline at end of file +fun Int.spacerDollar(str: Any): String = "$this$$str" + +fun Int.getDefaultGroupId() = this.spacerDollar("read_you_app_default_group") \ No newline at end of file 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 84eb4a8..f0a9765 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 @@ -17,6 +17,7 @@ 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.drawer.group.GroupOptionDrawer 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 @@ -165,4 +166,5 @@ fun HomePage( } FeedOptionDrawer() + GroupOptionDrawer() } \ 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 index d9d3d99..99c3623 100644 --- 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 @@ -27,7 +27,7 @@ fun DeleteFeedDialog( val context = LocalContext.current val viewState = viewModel.viewState.collectAsStateValue() val scope = rememberCoroutineScope() - val deletedTip = stringResource(R.string.has_been_deleted, feedName) + val toastString = stringResource(R.string.delete_toast, feedName) Dialog( visible = viewState.deleteDialogVisible, @@ -37,7 +37,7 @@ fun DeleteFeedDialog( icon = { Icon( imageVector = Icons.Outlined.DeleteForever, - contentDescription = stringResource(R.string.subscribe), + contentDescription = stringResource(R.string.unsubscribe), ) }, title = { @@ -52,7 +52,7 @@ fun DeleteFeedDialog( viewModel.dispatch(FeedOptionViewAction.Delete { viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) viewModel.dispatch(FeedOptionViewAction.Hide(scope)) - Toast.makeText(context, deletedTip, Toast.LENGTH_SHORT).show() + Toast.makeText(context, toastString, Toast.LENGTH_SHORT).show() }) } ) { diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllAllowNotificationDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllAllowNotificationDialog.kt new file mode 100644 index 0000000..f4ea5d7 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllAllowNotificationDialog.kt @@ -0,0 +1,81 @@ +package me.ash.reader.ui.page.home.drawer.group + +import android.widget.Toast +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Notifications +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.component.Dialog +import me.ash.reader.ui.ext.collectAsStateValue + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun AllAllowNotificationDialog( + modifier: Modifier = Modifier, + groupName: String, + viewModel: GroupOptionViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val viewState = viewModel.viewState.collectAsStateValue() + val scope = rememberCoroutineScope() + val allowToastString = stringResource(R.string.all_allow_notification_toast, groupName) + val denyToastString = stringResource(R.string.all_deny_notification_toast, groupName) + + Dialog( + visible = viewState.allAllowNotificationDialogVisible, + onDismissRequest = { + viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) + }, + icon = { + Icon( + imageVector = Icons.Outlined.Notifications, + contentDescription = stringResource(R.string.allow_notification), + ) + }, + title = { + Text(text = stringResource(R.string.allow_notification)) + }, + text = { + Text(text = stringResource(R.string.all_allow_notification_tip, groupName)) + }, + confirmButton = { + TextButton( + onClick = { + viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(true) { + viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) + viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + Toast.makeText(context, allowToastString, Toast.LENGTH_SHORT).show() + }) + } + ) { + Text( + text = stringResource(R.string.allow), + ) + } + }, + dismissButton = { + TextButton( + onClick = { + viewModel.dispatch(GroupOptionViewAction.AllAllowNotification(false) { + viewModel.dispatch(GroupOptionViewAction.HideAllAllowNotificationDialog) + viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + Toast.makeText(context, denyToastString, Toast.LENGTH_SHORT).show() + }) + } + ) { + Text( + text = stringResource(R.string.deny), + ) + } + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllParseFullContentDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllParseFullContentDialog.kt new file mode 100644 index 0000000..b7526f9 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/AllParseFullContentDialog.kt @@ -0,0 +1,81 @@ +package me.ash.reader.ui.page.home.drawer.group + +import android.widget.Toast +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Article +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.component.Dialog +import me.ash.reader.ui.ext.collectAsStateValue + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun AllParseFullContentDialog( + modifier: Modifier = Modifier, + groupName: String, + viewModel: GroupOptionViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val viewState = viewModel.viewState.collectAsStateValue() + val scope = rememberCoroutineScope() + val allowToastString = stringResource(R.string.all_parse_full_content_toast, groupName) + val denyToastString = stringResource(R.string.all_deny_parse_full_content_toast, groupName) + + Dialog( + visible = viewState.allParseFullContentDialogVisible, + onDismissRequest = { + viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) + }, + icon = { + Icon( + imageVector = Icons.Outlined.Article, + contentDescription = stringResource(R.string.parse_full_content), + ) + }, + title = { + Text(text = stringResource(R.string.parse_full_content)) + }, + text = { + Text(text = stringResource(R.string.all_parse_full_content_tip, groupName)) + }, + confirmButton = { + TextButton( + onClick = { + viewModel.dispatch(GroupOptionViewAction.AllParseFullContent(true) { + viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) + viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + Toast.makeText(context, allowToastString, Toast.LENGTH_SHORT).show() + }) + } + ) { + Text( + text = stringResource(R.string.allow), + ) + } + }, + dismissButton = { + TextButton( + onClick = { + viewModel.dispatch(GroupOptionViewAction.AllParseFullContent(false) { + viewModel.dispatch(GroupOptionViewAction.HideAllParseFullContentDialog) + viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + Toast.makeText(context, denyToastString, Toast.LENGTH_SHORT).show() + }) + } + ) { + Text( + text = stringResource(R.string.deny), + ) + } + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/DeleteGroupDialog.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/DeleteGroupDialog.kt new file mode 100644 index 0000000..ee2652e --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/DeleteGroupDialog.kt @@ -0,0 +1,76 @@ +package me.ash.reader.ui.page.home.drawer.group + +import android.widget.Toast +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.DeleteForever +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.component.Dialog +import me.ash.reader.ui.ext.collectAsStateValue + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun DeleteGroupDialog( + modifier: Modifier = Modifier, + groupName: String, + viewModel: GroupOptionViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val viewState = viewModel.viewState.collectAsStateValue() + val scope = rememberCoroutineScope() + val toastString = stringResource(R.string.delete_toast, groupName) + + Dialog( + visible = viewState.deleteDialogVisible, + onDismissRequest = { + viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) + }, + icon = { + Icon( + imageVector = Icons.Outlined.DeleteForever, + contentDescription = stringResource(R.string.delete_group), + ) + }, + title = { + Text(text = stringResource(R.string.delete_group)) + }, + text = { + Text(text = stringResource(R.string.delete_group_tip, groupName)) + }, + confirmButton = { + TextButton( + onClick = { + viewModel.dispatch(GroupOptionViewAction.Delete { + viewModel.dispatch(GroupOptionViewAction.HideDeleteDialog) + viewModel.dispatch(GroupOptionViewAction.Hide(scope)) + Toast.makeText(context, toastString, Toast.LENGTH_SHORT).show() + }) + } + ) { + Text( + text = stringResource(R.string.delete), + ) + } + }, + dismissButton = { + TextButton( + onClick = { + viewModel.dispatch(GroupOptionViewAction.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/group/GroupOptionDrawer.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionDrawer.kt new file mode 100644 index 0000000..2db3335 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionDrawer.kt @@ -0,0 +1,155 @@ +package me.ash.reader.ui.page.home.drawer.group + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Article +import androidx.compose.material.icons.outlined.Folder +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.Alignment +import androidx.compose.ui.Modifier +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 com.google.accompanist.flowlayout.FlowRow +import com.google.accompanist.flowlayout.MainAxisAlignment +import me.ash.reader.R +import me.ash.reader.ui.component.BottomDrawer +import me.ash.reader.ui.component.SelectionChip +import me.ash.reader.ui.component.Subtitle +import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.ext.currentAccountId +import me.ash.reader.ui.ext.getDefaultGroupId +import me.ash.reader.ui.ext.roundClick + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun GroupOptionDrawer( + modifier: Modifier = Modifier, + GroupOptionViewModel: GroupOptionViewModel = hiltViewModel(), + content: @Composable () -> Unit = {}, +) { + val context = LocalContext.current + val viewState = GroupOptionViewModel.viewState.collectAsStateValue() + val group = viewState.group + + BottomDrawer( + drawerState = viewState.drawerState, + sheetContent = { + Column { + Icon( + modifier = modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + imageVector = Icons.Outlined.Folder, + contentDescription = group?.name ?: stringResource(R.string.unknown), + tint = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = modifier.height(16.dp)) + Text( + modifier = Modifier + .roundClick {} + .fillMaxWidth(), + text = group?.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)) + Column( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.group_option_tip), + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + } + Spacer(modifier = Modifier.height(26.dp)) + + Subtitle(text = stringResource(R.string.preset)) + Spacer(modifier = Modifier.height(10.dp)) + FlowRow( + mainAxisAlignment = MainAxisAlignment.Start, + crossAxisSpacing = 10.dp, + mainAxisSpacing = 10.dp, + ) { + SelectionChip( + modifier = Modifier.animateContentSize(), + content = stringResource(R.string.allow_notification), + selected = false, + selectedIcon = { + Icon( + imageVector = Icons.Outlined.Notifications, + contentDescription = stringResource(R.string.allow_notification), + modifier = Modifier + .padding(start = 8.dp) + .size(20.dp), + ) + }, + ) { + GroupOptionViewModel.dispatch(GroupOptionViewAction.ShowAllAllowNotificationDialog) + } + SelectionChip( + modifier = Modifier.animateContentSize(), + content = stringResource(R.string.parse_full_content), + selected = false, + selectedIcon = { + Icon( + imageVector = Icons.Outlined.Article, + contentDescription = stringResource(R.string.parse_full_content), + modifier = Modifier + .padding(start = 8.dp) + .size(20.dp), + ) + }, + ) { + GroupOptionViewModel.dispatch(GroupOptionViewAction.ShowAllParseFullContentDialog) + } + if (group?.id != context.currentAccountId.getDefaultGroupId()) { + SelectionChip( + modifier = Modifier.animateContentSize(), + content = stringResource(R.string.delete_group), + selected = false, + ) { + GroupOptionViewModel.dispatch(GroupOptionViewAction.ShowDeleteDialog) + } + } + } + Spacer(modifier = Modifier.height(26.dp)) + +// AddToGroup( +// groups = groups, +// selectedGroupId = selectedGroupId, +// onGroupClick = onGroupClick, +// onAddNewGroup = onAddNewGroup, +// ) + Spacer(modifier = Modifier.height(6.dp)) + } + } + } + ) { + content() + } + + DeleteGroupDialog(groupName = group?.name ?: "") + AllAllowNotificationDialog(groupName = group?.name ?: "") + AllParseFullContentDialog(groupName = group?.name ?: "") +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionViewModel.kt new file mode 100644 index 0000000..84a815b --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/drawer/group/GroupOptionViewModel.kt @@ -0,0 +1,189 @@ +package me.ash.reader.ui.page.home.drawer.group + +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.Dispatchers +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 kotlinx.coroutines.withContext +import me.ash.reader.data.entity.Group +import me.ash.reader.data.repository.RssRepository +import javax.inject.Inject + +@OptIn( + ExperimentalPagerApi::class, + ExperimentalMaterialApi::class +) +@HiltViewModel +class GroupOptionViewModel @Inject constructor( + private val rssRepository: RssRepository, +) : ViewModel() { + private val _viewState = MutableStateFlow(GroupOptionViewState()) + val viewState: StateFlow = _viewState.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + rssRepository.get().pullGroups().collect { groups -> + _viewState.update { + it.copy( + groups = groups + ) + } + } + } + } + + fun dispatch(action: GroupOptionViewAction) { + when (action) { + is GroupOptionViewAction.Show -> show(action.scope, action.groupId) + is GroupOptionViewAction.Hide -> hide(action.scope) + is GroupOptionViewAction.ShowDeleteDialog -> changeDeleteDialogVisible(true) + is GroupOptionViewAction.HideDeleteDialog -> changeDeleteDialogVisible(false) + is GroupOptionViewAction.Delete -> delete(action.callback) + is GroupOptionViewAction.ShowAllAllowNotificationDialog -> + changeAllAllowNotificationDialogVisible(true) + is GroupOptionViewAction.HideAllAllowNotificationDialog -> + changeAllAllowNotificationDialogVisible(false) + is GroupOptionViewAction.AllAllowNotification -> + allAllowNotification(action.isNotification, action.callback) + is GroupOptionViewAction.ShowAllParseFullContentDialog -> + changeAllParseFullContentDialogVisible(true) + is GroupOptionViewAction.HideAllParseFullContentDialog -> + changeAllParseFullContentDialogVisible(false) + is GroupOptionViewAction.AllParseFullContent -> + allParseFullContent(action.isFullContent, action.callback) + } + } + + private suspend fun fetchGroup(groupId: String) { + val group = rssRepository.get().findGroupById(groupId) + _viewState.update { + it.copy( + group = group, + ) + } + } + + private fun show(scope: CoroutineScope, groupId: String) { + scope.launch { + fetchGroup(groupId) + _viewState.value.drawerState.show() + } + } + + private fun hide(scope: CoroutineScope) { + scope.launch { + _viewState.value.drawerState.hide() + } + } + + private fun allAllowNotification(isNotification: Boolean, callback: () -> Unit = {}) { + _viewState.value.group?.let { + viewModelScope.launch(Dispatchers.IO) { + rssRepository.get().groupAllowNotification(it, isNotification) + withContext(Dispatchers.Main) { + callback() + } + } + } + } + + private fun changeAllAllowNotificationDialogVisible(visible: Boolean) { + _viewState.update { + it.copy( + allAllowNotificationDialogVisible = visible, + ) + } + } + + private fun allParseFullContent(isFullContent: Boolean, callback: () -> Unit = {}) { + _viewState.value.group?.let { + viewModelScope.launch(Dispatchers.IO) { + rssRepository.get().groupParseFullContent(it, isFullContent) + withContext(Dispatchers.Main) { + callback() + } + } + } + } + + private fun changeAllParseFullContentDialogVisible(visible: Boolean) { + _viewState.update { + it.copy( + allParseFullContentDialogVisible = visible, + ) + } + } + + private fun delete(callback: () -> Unit = {}) { + _viewState.value.group?.let { + viewModelScope.launch(Dispatchers.IO) { + rssRepository.get().deleteGroup(it) + withContext(Dispatchers.Main) { + callback() + } + } + } + } + + private fun changeDeleteDialogVisible(visible: Boolean) { + _viewState.update { + it.copy( + deleteDialogVisible = visible, + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +data class GroupOptionViewState( + var drawerState: ModalBottomSheetState = ModalBottomSheetState(ModalBottomSheetValue.Hidden), + val group: Group? = null, + val groups: List = emptyList(), + val allAllowNotificationDialogVisible: Boolean = false, + val allParseFullContentDialogVisible: Boolean = false, + val deleteDialogVisible: Boolean = false, +) + +sealed class GroupOptionViewAction { + data class Show( + val scope: CoroutineScope, + val groupId: String + ) : GroupOptionViewAction() + + data class Hide( + val scope: CoroutineScope, + ) : GroupOptionViewAction() + + data class Delete( + val callback: () -> Unit = {} + ) : GroupOptionViewAction() + + object ShowDeleteDialog : GroupOptionViewAction() + object HideDeleteDialog : GroupOptionViewAction() + + data class AllParseFullContent( + val isFullContent: Boolean, + val callback: () -> Unit = {} + ) : GroupOptionViewAction() + + object ShowAllParseFullContentDialog : GroupOptionViewAction() + object HideAllParseFullContentDialog : GroupOptionViewAction() + + data class AllAllowNotification( + val isNotification: Boolean, + val callback: () -> Unit = {} + ) : GroupOptionViewAction() + + object ShowAllAllowNotificationDialog : GroupOptionViewAction() + object HideAllAllowNotificationDialog : GroupOptionViewAction() +} 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 6b76605..083bc5f 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 @@ -186,7 +186,7 @@ fun FeedsPage( // Crossfade(targetState = groupWithFeed) { groupWithFeed -> Column { GroupItem( - text = groupWithFeed.group.name, + group = groupWithFeed.group, feeds = groupWithFeed.feeds, groupOnClick = { onFilterChange( 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 25e852a..d906160 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 @@ -1,5 +1,6 @@ package me.ash.reader.ui.page.home.feeds +import android.view.HapticFeedbackConstants import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -18,22 +19,30 @@ import androidx.compose.runtime.* 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.res.stringResource 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.data.entity.Feed +import me.ash.reader.data.entity.Group +import me.ash.reader.ui.page.home.drawer.group.GroupOptionViewAction +import me.ash.reader.ui.page.home.drawer.group.GroupOptionViewModel @OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun GroupItem( modifier: Modifier = Modifier, - text: String, + group: Group, feeds: List, isExpanded: Boolean = true, + groupOptionViewModel: GroupOptionViewModel = hiltViewModel(), groupOnClick: () -> Unit = {}, feedOnClick: (feed: Feed) -> Unit = {}, ) { + val view = LocalView.current + val scope = rememberCoroutineScope() var expanded by remember { mutableStateOf(isExpanded) } Column( @@ -47,6 +56,8 @@ fun GroupItem( groupOnClick() }, onLongClick = { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id)) } ) .padding(top = 22.dp) @@ -60,7 +71,7 @@ fun GroupItem( modifier = Modifier .weight(1f) .padding(start = 28.dp), - text = text, + text = group.name, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSecondaryContainer, maxLines = 1, diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6c37e7f..bc090f2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -12,6 +12,8 @@ 展开 确认 取消 + 允许 + 拒绝 默认 未知 返回 @@ -29,16 +31,25 @@ 预设 已选择 允许通知 + 允许 \"%1$s\" 分组中的所有订阅源发出通知。 + 已全部允许 \"%1$s\" 分组中的通知 + 已全部拒绝 \"%1$s\" 分组中的通知 全文解析 + 对 \"%1$s\" 分组中的所有文章进行全文解析。 + 全文解析 \"%1$s\" 分组中的文章 + 不再全文解析 \"%1$s\" 分组中的文章 添加到组 新建分组 名称 打开 %1$s 选项 删除 - \"%1$s\" 已被删除 + \"%1$s\" 已被删除 取消订阅 - 不再订阅 \"%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 558cfc2..fe04280 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,8 @@ Expand More Confirm Cancel + Allow + Deny Default Unknown Back @@ -29,16 +31,25 @@ Preset Selected Allow Notification + Allow all feeds in the \"%1$s\" group to send notifications. + All notifications in the \"%1$s\" group are allowed + All notifications in the \"%1$s\" group are denied Parse Full Content + Full content parsing of all articles in the \"%1$s\" group. + Full content parsing of all articles in the \"%1$s\" group + No more full content parsing of all articles in the \"%1$s\" group Add to Group Create New Group Name Open %1$s Options Delete - \"%1$s\" has been deleted + \"%1$s\" has been deleted Unsubscribe - Unsubscribe \"%1$s\" and delete all its archived articles. + Unsubscribe \"%1$s\" and delete all archived articles in it. + Delete Group + Delete the \"%1$s\" group, and delete all feeds and archived articles in it. + The following options will be applied to all feeds in this group. Today Yesterday %1$s At %2$s