mirror of https://github.com/readrops/Readrops.git
Add notifications screen
This commit is contained in:
parent
3f43d2219c
commit
3880fb1fc5
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
}
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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?,
|
||||
|
|
Loading…
Reference in New Issue