Add GroupOptionView

This commit is contained in:
Ash 2022-04-05 04:53:44 +08:00
parent 590470137a
commit 1ba149368b
20 changed files with 714 additions and 29 deletions

View File

@ -69,6 +69,7 @@ class App : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
applicationScope.launch(dispatcherDefault) { applicationScope.launch(dispatcherDefault) {
accountInit() accountInit()
dataStoreInit()
workerInit() workerInit()
} }
} }
@ -81,6 +82,9 @@ class App : Application(), Configuration.Provider {
} }
} }
private fun dataStoreInit() {
}
private fun workerInit() { private fun workerInit() {
rssRepository.get().doSync() rssRepository.get().doSync()
} }

View File

@ -56,6 +56,20 @@ interface ArticleDao {
) )
suspend fun deleteByFeedId(accountId: Int, feedId: String) 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 @Transaction
@Query( @Query(
""" """

View File

@ -5,6 +5,41 @@ import me.ash.reader.data.entity.Feed
@Dao @Dao
interface FeedDao { 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( @Query(
""" """
SELECT * FROM feed SELECT * FROM feed

View File

@ -13,7 +13,7 @@ interface GroupDao {
WHERE id = :id WHERE id = :id
""" """
) )
fun queryById(id: String): Group? suspend fun queryById(id: String): Group?
@Transaction @Transaction
@Query( @Query(
@ -31,7 +31,7 @@ interface GroupDao {
WHERE accountId = :accountId WHERE accountId = :accountId
""" """
) )
fun queryAllGroupWithFeed(accountId: Int): List<GroupWithFeed> suspend fun queryAllGroupWithFeed(accountId: Int): List<GroupWithFeed>
@Query( @Query(
""" """

View File

@ -112,6 +112,10 @@ abstract class AbstractRssRepository constructor(
return feedDao.queryById(id) return feedDao.queryById(id)
} }
suspend fun findGroupById(id: String): Group? {
return groupDao.queryById(id)
}
suspend fun findArticleById(id: String): ArticleWithFeed? { suspend fun findArticleById(id: String): ArticleWithFeed? {
return articleDao.queryById(id) return articleDao.queryById(id)
} }
@ -133,13 +137,23 @@ abstract class AbstractRssRepository constructor(
} }
suspend fun deleteGroup(group: Group) { 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) { suspend fun deleteFeed(feed: Feed) {
articleDao.deleteByFeedId(context.currentAccountId, feed.id) articleDao.deleteByFeedId(context.currentAccountId, feed.id)
feedDao.delete(feed) 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 @HiltWorker

View File

@ -8,7 +8,7 @@ import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Account import me.ash.reader.data.entity.Account
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.ui.ext.currentAccountId 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 import javax.inject.Inject
class AccountRepository @Inject constructor( class AccountRepository @Inject constructor(
@ -38,7 +38,7 @@ class AccountRepository @Inject constructor(
if (groupDao.queryAll(it.id!!).isEmpty()) { if (groupDao.queryAll(it.id!!).isEmpty()) {
groupDao.insert( groupDao.insert(
Group( Group(
id = it.id!!.spacerDollar(readYouString + defaultString), id = it.id!!.getDefaultGroupId(),
name = defaultString, name = defaultString,
accountId = it.id!!, accountId = it.id!!,
) )

View File

@ -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.repository.SyncWorker.Companion.setIsSyncing
import me.ash.reader.data.source.RssNetworkDataSource import me.ash.reader.data.source.RssNetworkDataSource
import me.ash.reader.ui.ext.currentAccountId 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.ExtraName
import me.ash.reader.ui.page.common.NotificationGroupName import me.ash.reader.ui.page.common.NotificationGroupName
import java.util.* import java.util.*
@ -81,14 +82,16 @@ class LocalRssRepository @Inject constructor(
} }
override suspend fun addGroup(name: String): String { override suspend fun addGroup(name: String): String {
return UUID.randomUUID().toString().also { context.currentAccountId.let { accountId ->
groupDao.insert( return accountId.spacerDollar(UUID.randomUUID().toString()).also {
Group( groupDao.insert(
id = it, Group(
name = name, id = it,
accountId = context.currentAccountId name = name,
accountId = accountId
)
) )
) }
} }
} }

View File

@ -7,14 +7,13 @@ import be.ceau.opml.entity.Head
import be.ceau.opml.entity.Opml import be.ceau.opml.entity.Opml
import be.ceau.opml.entity.Outline import be.ceau.opml.entity.Outline
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.R
import me.ash.reader.data.dao.AccountDao import me.ash.reader.data.dao.AccountDao
import me.ash.reader.data.dao.FeedDao import me.ash.reader.data.dao.FeedDao
import me.ash.reader.data.dao.GroupDao import me.ash.reader.data.dao.GroupDao
import me.ash.reader.data.entity.Feed import me.ash.reader.data.entity.Feed
import me.ash.reader.data.source.OpmlLocalDataSource import me.ash.reader.data.source.OpmlLocalDataSource
import me.ash.reader.ui.ext.currentAccountId 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.io.InputStream
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +26,6 @@ class OpmlRepository @Inject constructor(
private val accountDao: AccountDao, private val accountDao: AccountDao,
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val opmlLocalDataSource: OpmlLocalDataSource, private val opmlLocalDataSource: OpmlLocalDataSource,
private val stringsRepository: StringsRepository,
) { ) {
@Throws(Exception::class) @Throws(Exception::class)
suspend fun saveToDatabase(inputStream: InputStream) { suspend fun saveToDatabase(inputStream: InputStream) {
@ -88,8 +86,6 @@ class OpmlRepository @Inject constructor(
} }
private fun getDefaultGroupId(): String { private fun getDefaultGroupId(): String {
val readYouString = stringsRepository.getString(R.string.read_you) return context.currentAccountId.getDefaultGroupId()
val defaultString = stringsRepository.getString(R.string.defaults)
return context.currentAccountId.spacerDollar(readYouString + defaultString)
} }
} }

View File

@ -1,3 +1,5 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
fun Int.spacerDollar(str: Any): String = "$this$$str" fun Int.spacerDollar(str: Any): String = "$this$$str"
fun Int.getDefaultGroupId() = this.spacerDollar("read_you_app_default_group")

View File

@ -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.FeedOptionDrawer
import me.ash.reader.ui.page.home.drawer.feed.FeedOptionViewAction 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.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.feeds.FeedsPage
import me.ash.reader.ui.page.home.flow.FlowPage import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.read.ReadPage import me.ash.reader.ui.page.home.read.ReadPage
@ -165,4 +166,5 @@ fun HomePage(
} }
FeedOptionDrawer() FeedOptionDrawer()
GroupOptionDrawer()
} }

View File

@ -27,7 +27,7 @@ fun DeleteFeedDialog(
val context = LocalContext.current val context = LocalContext.current
val viewState = viewModel.viewState.collectAsStateValue() val viewState = viewModel.viewState.collectAsStateValue()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val deletedTip = stringResource(R.string.has_been_deleted, feedName) val toastString = stringResource(R.string.delete_toast, feedName)
Dialog( Dialog(
visible = viewState.deleteDialogVisible, visible = viewState.deleteDialogVisible,
@ -37,7 +37,7 @@ fun DeleteFeedDialog(
icon = { icon = {
Icon( Icon(
imageVector = Icons.Outlined.DeleteForever, imageVector = Icons.Outlined.DeleteForever,
contentDescription = stringResource(R.string.subscribe), contentDescription = stringResource(R.string.unsubscribe),
) )
}, },
title = { title = {
@ -52,7 +52,7 @@ fun DeleteFeedDialog(
viewModel.dispatch(FeedOptionViewAction.Delete { viewModel.dispatch(FeedOptionViewAction.Delete {
viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog) viewModel.dispatch(FeedOptionViewAction.HideDeleteDialog)
viewModel.dispatch(FeedOptionViewAction.Hide(scope)) viewModel.dispatch(FeedOptionViewAction.Hide(scope))
Toast.makeText(context, deletedTip, Toast.LENGTH_SHORT).show() Toast.makeText(context, toastString, Toast.LENGTH_SHORT).show()
}) })
} }
) { ) {

View File

@ -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),
)
}
},
)
}

View File

@ -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),
)
}
},
)
}

View File

@ -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),
)
}
},
)
}

View File

@ -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 ?: "")
}

View File

@ -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<GroupOptionViewState> = _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<Group> = 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()
}

View File

@ -186,7 +186,7 @@ fun FeedsPage(
// Crossfade(targetState = groupWithFeed) { groupWithFeed -> // Crossfade(targetState = groupWithFeed) { groupWithFeed ->
Column { Column {
GroupItem( GroupItem(
text = groupWithFeed.group.name, group = groupWithFeed.group,
feeds = groupWithFeed.feeds, feeds = groupWithFeed.feeds,
groupOnClick = { groupOnClick = {
onFilterChange( onFilterChange(

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import android.view.HapticFeedbackConstants
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -18,22 +19,30 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Feed 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) @OptIn(ExperimentalMaterialApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
@Composable @Composable
fun GroupItem( fun GroupItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
text: String, group: Group,
feeds: List<Feed>, feeds: List<Feed>,
isExpanded: Boolean = true, isExpanded: Boolean = true,
groupOptionViewModel: GroupOptionViewModel = hiltViewModel(),
groupOnClick: () -> Unit = {}, groupOnClick: () -> Unit = {},
feedOnClick: (feed: Feed) -> Unit = {}, feedOnClick: (feed: Feed) -> Unit = {},
) { ) {
val view = LocalView.current
val scope = rememberCoroutineScope()
var expanded by remember { mutableStateOf(isExpanded) } var expanded by remember { mutableStateOf(isExpanded) }
Column( Column(
@ -47,6 +56,8 @@ fun GroupItem(
groupOnClick() groupOnClick()
}, },
onLongClick = { onLongClick = {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
groupOptionViewModel.dispatch(GroupOptionViewAction.Show(scope, group.id))
} }
) )
.padding(top = 22.dp) .padding(top = 22.dp)
@ -60,7 +71,7 @@ fun GroupItem(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 28.dp), .padding(start = 28.dp),
text = text, text = group.name,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer, color = MaterialTheme.colorScheme.onSecondaryContainer,
maxLines = 1, maxLines = 1,

View File

@ -12,6 +12,8 @@
<string name="expand_more">展开</string> <string name="expand_more">展开</string>
<string name="confirm">确认</string> <string name="confirm">确认</string>
<string name="cancel">取消</string> <string name="cancel">取消</string>
<string name="allow">允许</string>
<string name="deny">拒绝</string>
<string name="defaults">默认</string> <string name="defaults">默认</string>
<string name="unknown">未知</string> <string name="unknown">未知</string>
<string name="back">返回</string> <string name="back">返回</string>
@ -29,16 +31,25 @@
<string name="preset">预设</string> <string name="preset">预设</string>
<string name="selected">已选择</string> <string name="selected">已选择</string>
<string name="allow_notification">允许通知</string> <string name="allow_notification">允许通知</string>
<string name="all_allow_notification_tip">允许 \"%1$s\" 分组中的所有订阅源发出通知。</string>
<string name="all_allow_notification_toast">已全部允许 \"%1$s\" 分组中的通知</string>
<string name="all_deny_notification_toast">已全部拒绝 \"%1$s\" 分组中的通知</string>
<string name="parse_full_content">全文解析</string> <string name="parse_full_content">全文解析</string>
<string name="all_parse_full_content_tip">对 \"%1$s\" 分组中的所有文章进行全文解析。</string>
<string name="all_parse_full_content_toast">全文解析 \"%1$s\" 分组中的文章</string>
<string name="all_deny_parse_full_content_toast">不再全文解析 \"%1$s\" 分组中的文章</string>
<string name="add_to_group">添加到组</string> <string name="add_to_group">添加到组</string>
<string name="create_new_group">新建分组</string> <string name="create_new_group">新建分组</string>
<string name="name">名称</string> <string name="name">名称</string>
<string name="open_with">打开 %1$s</string> <string name="open_with">打开 %1$s</string>
<string name="options">选项</string> <string name="options">选项</string>
<string name="delete">删除</string> <string name="delete">删除</string>
<string name="has_been_deleted">\"%1$s\" 已被删除</string> <string name="delete_toast">\"%1$s\" 已被删除</string>
<string name="unsubscribe">取消订阅</string> <string name="unsubscribe">取消订阅</string>
<string name="unsubscribe_tip">不再订阅 \"%1$s\",同时删除其所有已归档的文章。</string> <string name="unsubscribe_tip">不再订阅 \"%1$s\",同时删除其中所有已归档的文章。</string>
<string name="delete_group">删除分组</string>
<string name="delete_group_tip">删除 \"%1$s\" 分组,同时删除其中所有订阅源和已归档的文章。</string>
<string name="group_option_tip">以下选项将应用到该分组中的所有订阅源。</string>
<string name="today">今天</string> <string name="today">今天</string>
<string name="yesterday">昨天</string> <string name="yesterday">昨天</string>
<string name="date_at_time">%1$s %2$s</string> <string name="date_at_time">%1$s %2$s</string>

View File

@ -12,6 +12,8 @@
<string name="expand_more">Expand More</string> <string name="expand_more">Expand More</string>
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="allow">Allow</string>
<string name="deny">Deny</string>
<string name="defaults">Default</string> <string name="defaults">Default</string>
<string name="unknown">Unknown</string> <string name="unknown">Unknown</string>
<string name="back">Back</string> <string name="back">Back</string>
@ -29,16 +31,25 @@
<string name="preset">Preset</string> <string name="preset">Preset</string>
<string name="selected">Selected</string> <string name="selected">Selected</string>
<string name="allow_notification">Allow Notification</string> <string name="allow_notification">Allow Notification</string>
<string name="all_allow_notification_tip">Allow all feeds in the \"%1$s\" group to send notifications.</string>
<string name="all_allow_notification_toast">All notifications in the \"%1$s\" group are allowed</string>
<string name="all_deny_notification_toast">All notifications in the \"%1$s\" group are denied</string>
<string name="parse_full_content">Parse Full Content</string> <string name="parse_full_content">Parse Full Content</string>
<string name="all_parse_full_content_tip">Full content parsing of all articles in the \"%1$s\" group.</string>
<string name="all_parse_full_content_toast">Full content parsing of all articles in the \"%1$s\" group</string>
<string name="all_deny_parse_full_content_toast">No more full content parsing of all articles in the \"%1$s\" group</string>
<string name="add_to_group">Add to Group</string> <string name="add_to_group">Add to Group</string>
<string name="create_new_group">Create New Group</string> <string name="create_new_group">Create New Group</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="open_with">Open %1$s</string> <string name="open_with">Open %1$s</string>
<string name="options">Options</string> <string name="options">Options</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="has_been_deleted">\"%1$s\" has been deleted</string> <string name="delete_toast">\"%1$s\" has been deleted</string>
<string name="unsubscribe">Unsubscribe</string> <string name="unsubscribe">Unsubscribe</string>
<string name="unsubscribe_tip">Unsubscribe \"%1$s\" and delete all its archived articles.</string> <string name="unsubscribe_tip">Unsubscribe \"%1$s\" and delete all archived articles in it.</string>
<string name="delete_group">Delete Group</string>
<string name="delete_group_tip">Delete the \"%1$s\" group, and delete all feeds and archived articles in it.</string>
<string name="group_option_tip">The following options will be applied to all feeds in this group.</string>
<string name="today">Today</string> <string name="today">Today</string>
<string name="yesterday">Yesterday</string> <string name="yesterday">Yesterday</string>
<string name="date_at_time">%1$s At %2$s</string> <string name="date_at_time">%1$s At %2$s</string>