Add notifications screen

This commit is contained in:
Shinokuni 2024-07-02 13:18:50 +02:00
parent 3f43d2219c
commit 3880fb1fc5
10 changed files with 354 additions and 2 deletions

View File

@ -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

View File

@ -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(

View File

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

View File

@ -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<NotificationsScreenModel> { 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)
}
}
)
}
}
}
}
}

View File

@ -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>(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<FeedWithFolder2> = emptyList()
) {
val allFeedNotificationsEnabled
get() = feedsWithFolder.none { !it.feed.isNotificationEnabled }
}

View File

@ -166,4 +166,6 @@
<string name="http_error">Erreur HTTP %1$d</string>
<string name="feed_url_read_only">L\'API ne permet pas de modifier l\'URL</string>
<string name="updating_account">Mise à jour du compte %1$s</string>
<string name="enable_all">Tout activer</string>
<string name="disable_all">Tout désactiver</string>
</resources>

View File

@ -172,4 +172,6 @@
<string name="http_error">HTTP error %1$d</string>
<string name="feed_url_read_only">The API doesn\'t support Feed URL modification</string>
<string name="updating_account">Updating %1$s account</string>
<string name="enable_all">Enable all</string>
<string name="disable_all">Disable all</string>
</resources>

View File

@ -22,4 +22,10 @@ interface NewAccountDao : NewBaseDao<Account> {
@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<Boolean>
}

View File

@ -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<Feed> {
@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<List<FeedWithFolder2>>
@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
*

View File

@ -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?,