From 3880fb1fc5974bc128d4a4d208c4a52f122c035a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 2 Jul 2024 13:18:50 +0200 Subject: [PATCH] Add notifications screen --- .../readrops/app/compose/ComposeAppModule.kt | 3 + .../app/compose/account/AccountTab.kt | 3 +- .../compose/notifications/NotificationItem.kt | 92 ++++++++++ .../notifications/NotificationsScreen.kt | 167 ++++++++++++++++++ .../notifications/NotificationsScreenModel.kt | 62 +++++++ appcompose/src/main/res/values-fr/strings.xml | 2 + appcompose/src/main/res/values/strings.xml | 2 + .../readrops/db/dao/newdao/NewAccountDao.kt | 6 + .../com/readrops/db/dao/newdao/NewFeedDao.kt | 11 ++ .../com/readrops/db/pojo/FeedWithFolder.kt | 8 +- 10 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationItem.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreen.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreenModel.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index f6f444ab..5ffa7b1c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -9,6 +9,7 @@ import com.readrops.app.compose.account.credentials.AccountCredentialsScreenMode import com.readrops.app.compose.account.selection.AccountSelectionScreenModel import com.readrops.app.compose.feeds.FeedScreenModel import com.readrops.app.compose.item.ItemScreenModel +import com.readrops.app.compose.notifications.NotificationsScreenModel import com.readrops.app.compose.repositories.BaseRepository import com.readrops.app.compose.repositories.FreshRSSRepository import com.readrops.app.compose.repositories.GetFoldersWithFeeds @@ -37,6 +38,8 @@ val composeAppModule = module { AccountCredentialsScreenModel(accountType, mode, get()) } + factory { (account: Account) -> NotificationsScreenModel(account, get()) } + single { GetFoldersWithFeeds(get()) } // repositories diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index 30fa8bcc..745e9936 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -50,6 +50,7 @@ import com.readrops.app.compose.account.credentials.AccountCredentialsScreenMode import com.readrops.app.compose.account.selection.AccountSelectionDialog import com.readrops.app.compose.account.selection.AccountSelectionScreen import com.readrops.app.compose.account.selection.adaptiveIconPainterResource +import com.readrops.app.compose.notifications.NotificationsScreen import com.readrops.app.compose.timelime.ErrorListDialog import com.readrops.app.compose.util.components.ErrorDialog import com.readrops.app.compose.util.components.SelectableIconText @@ -302,7 +303,7 @@ object AccountTab : Tab { style = MaterialTheme.typography.titleMedium, spacing = MaterialTheme.spacing.mediumSpacing, padding = MaterialTheme.spacing.mediumSpacing, - onClick = { } + onClick = { navigator.push(NotificationsScreen(state.account)) } ) SelectableIconText( diff --git a/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationItem.kt b/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationItem.kt new file mode 100644 index 00000000..9300ca38 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationItem.kt @@ -0,0 +1,92 @@ +package com.readrops.app.compose.notifications + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.VeryShortSpacer +import com.readrops.app.compose.util.theme.spacing + +@Composable +fun NotificationItem( + feedName: String, + iconUrl: String?, + folderName: String?, + checked: Boolean, + enabled: Boolean, + onCheckChange: (Boolean) -> Unit, +) { + Box( + modifier = Modifier.clickable { onCheckChange(!checked) } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = MaterialTheme.spacing.mediumSpacing, + vertical = MaterialTheme.spacing.shortSpacing + ) + ) { + AsyncImage( + model = iconUrl, + contentDescription = feedName, + placeholder = painterResource(id = R.drawable.ic_rss_feed_grey), + error = painterResource(id = R.drawable.ic_rss_feed_grey), + modifier = Modifier.size(24.dp) + ) + + MediumSpacer() + + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier.weight(1f, fill = true) + ) { + Text( + text = feedName, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + + ) + + if (folderName != null) { + VeryShortSpacer() + + Text( + text = folderName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + + MediumSpacer() + + Switch( + checked = checked, + enabled = enabled, + onCheckedChange = onCheckChange, + ) + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreen.kt new file mode 100644 index 00000000..e2b40482 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreen.kt @@ -0,0 +1,167 @@ +package com.readrops.app.compose.notifications + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import cafe.adriel.voyager.koin.getScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.readrops.app.compose.R +import com.readrops.app.compose.util.components.AndroidScreen +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.entities.account.Account +import org.koin.core.parameter.parametersOf + +class NotificationsScreen(val account: Account) : AndroidScreen() { + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val screenModel = getScreenModel { parametersOf(account) } + + var isDropDownMenuExpanded by remember { mutableStateOf(false) } + + val state by screenModel.state.collectAsStateWithLifecycle() + + val topAppBarScrollBehavior = + TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(id = R.string.notifications)) }, + navigationIcon = { + IconButton( + onClick = { navigator.pop() } + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null + ) + } + }, + actions = { + Box { + IconButton( + onClick = { isDropDownMenuExpanded = isDropDownMenuExpanded.not() } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + ) + } + + DropdownMenu( + expanded = isDropDownMenuExpanded, + onDismissRequest = { isDropDownMenuExpanded = false }, + ) { + DropdownMenuItem( + text = { + Text( + text = if (state.allFeedNotificationsEnabled) { + stringResource(id = R.string.disable_all) + } else { + stringResource(id = R.string.enable_all) + } + ) + }, + onClick = { + isDropDownMenuExpanded = false + screenModel.setAllFeedsNotificationsState(enabled = !state.allFeedNotificationsEnabled) + } + ) + } + } + }, + scrollBehavior = topAppBarScrollBehavior + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) + ) { + item { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = MaterialTheme.spacing.mediumSpacing) + + ) { + Text( + text = stringResource(id = R.string.enable_notifications) + ) + + Switch( + checked = state.isNotificationsEnabled, + onCheckedChange = { screenModel.setAccountNotificationsState(it) } + ) + } + + MediumSpacer() + + Text( + text = stringResource(id = R.string.feeds), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = MaterialTheme.spacing.mediumSpacing) + ) + } + + items( + items = state.feedsWithFolder, + key = { it.feed.id } + ) { feedWithFolder -> + NotificationItem( + feedName = feedWithFolder.feed.name!!, + iconUrl = feedWithFolder.feed.iconUrl, + folderName = feedWithFolder.folderName, + checked = feedWithFolder.feed.isNotificationEnabled, + enabled = state.isNotificationsEnabled, + onCheckChange = { + if (state.isNotificationsEnabled) { + screenModel.setFeedNotificationsState(feedWithFolder.feed.id, it) + } + } + ) + } + } + } + + } +} + diff --git a/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreenModel.kt b/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreenModel.kt new file mode 100644 index 00000000..0a860db6 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/notifications/NotificationsScreenModel.kt @@ -0,0 +1,62 @@ +package com.readrops.app.compose.notifications + +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import com.readrops.db.Database +import com.readrops.db.entities.account.Account +import com.readrops.db.pojo.FeedWithFolder2 +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class NotificationsScreenModel( + private val account: Account, + private val database: Database, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : StateScreenModel(NotificationsState(isNotificationsEnabled = account.isNotificationsEnabled)) { + + init { + screenModelScope.launch(dispatcher) { + database.newAccountDao().selectAccountNotificationsState(account.id) + .collect { isNotificationsEnabled -> + mutableState.update { it.copy(isNotificationsEnabled = isNotificationsEnabled) } + } + } + + screenModelScope.launch(dispatcher) { + database.newFeedDao().selectFeedsWithFolderName(account.id) + .collect { feedsWithFolder -> + mutableState.update { it.copy(feedsWithFolder = feedsWithFolder) } + } + } + } + + fun setAccountNotificationsState(enabled: Boolean) { + screenModelScope.launch(dispatcher) { + database.newAccountDao().updateNotificationState(account.id, enabled) + } + } + + fun setFeedNotificationsState(feedId: Int, enabled: Boolean) { + screenModelScope.launch(dispatcher) { + database.newFeedDao().updateFeedNotificationState(feedId, enabled) + } + } + + fun setAllFeedsNotificationsState(enabled: Boolean) { + screenModelScope.launch(dispatcher) { + database.newFeedDao().updateAllFeedsNotificationState(account.id, enabled) + } + } + +} + +data class NotificationsState( + val isNotificationsEnabled: Boolean = false, + val feedsWithFolder: List = emptyList() +) { + + val allFeedNotificationsEnabled + get() = feedsWithFolder.none { !it.feed.isNotificationEnabled } +} \ No newline at end of file diff --git a/appcompose/src/main/res/values-fr/strings.xml b/appcompose/src/main/res/values-fr/strings.xml index dec05c53..a0314297 100644 --- a/appcompose/src/main/res/values-fr/strings.xml +++ b/appcompose/src/main/res/values-fr/strings.xml @@ -166,4 +166,6 @@ Erreur HTTP %1$d L\'API ne permet pas de modifier l\'URL Mise à jour du compte %1$s + Tout activer + Tout désactiver \ No newline at end of file diff --git a/appcompose/src/main/res/values/strings.xml b/appcompose/src/main/res/values/strings.xml index c9369076..45398c0c 100644 --- a/appcompose/src/main/res/values/strings.xml +++ b/appcompose/src/main/res/values/strings.xml @@ -172,4 +172,6 @@ HTTP error %1$d The API doesn\'t support Feed URL modification Updating %1$s account + Enable all + Disable all \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt index 3e9a59ef..00b8611e 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt @@ -22,4 +22,10 @@ interface NewAccountDao : NewBaseDao { @Query("Update Account set last_modified = :lastModified Where id = :accountId") suspend fun updateLastModified(lastModified: Long, accountId: Int) + + @Query("Update Account set notifications_enabled = :enabled Where id = :accountId") + suspend fun updateNotificationState(accountId: Int, enabled: Boolean) + + @Query("Select notifications_enabled From Account Where id = :accountId") + fun selectAccountNotificationsState(accountId: Int): Flow } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index ba2ee041..fdc8b8db 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -9,6 +9,7 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import com.readrops.db.pojo.FeedWithCount +import com.readrops.db.pojo.FeedWithFolder2 import kotlinx.coroutines.flow.Flow @Dao @@ -56,6 +57,16 @@ abstract class NewFeedDao : NewBaseDao { @Query("Update Feed set background_color = :color Where id = :feedId") abstract fun updateFeedColor(feedId: Int, color: Int) + @Query("""Select Feed.*, Folder.name as folder_name From Feed Left Join Folder On Feed.folder_id = Folder.id + Where Feed.account_id = :accountId Order By Feed.name, Folder.name""") + abstract fun selectFeedsWithFolderName(accountId: Int): Flow> + + @Query("Update Feed set notification_enabled = :enabled Where id = :feedId") + abstract suspend fun updateFeedNotificationState(feedId: Int, enabled: Boolean) + + @Query("Update Feed set notification_enabled = :enabled Where account_id = :accountId") + abstract suspend fun updateAllFeedsNotificationState(accountId: Int, enabled: Boolean) + /** * Insert, update and delete feeds by account * diff --git a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt index 9090d754..1301d98b 100644 --- a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt +++ b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt @@ -1,17 +1,23 @@ package com.readrops.db.pojo import android.os.Parcelable +import androidx.room.ColumnInfo import androidx.room.Embedded import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import kotlinx.parcelize.Parcelize -@Parcelize +@Parcelize //TODO delete data class FeedWithFolder( @Embedded(prefix = "feed_") val feed: Feed, @Embedded(prefix = "folder_") val folder: Folder, ) : Parcelable +data class FeedWithFolder2( + @Embedded val feed: Feed, + @ColumnInfo(name = "folder_name") val folderName: String? +) + data class FolderWithFeed( val folderId: Int?, val folderName: String?,