Compare commits

...

5 Commits

22 changed files with 208 additions and 98 deletions

View File

@ -273,17 +273,17 @@ public class FreshRSSDataSource {
*/
private Completable setItemsReadState(@NonNull FreshRSSSyncData syncData, @NonNull String token) {
Completable readItemsCompletable;
if (syncData.getReadItemsIds().isEmpty()) {
if (syncData.getReadIds().isEmpty()) {
readItemsCompletable = Completable.complete();
} else {
readItemsCompletable = setItemsReadState(true, syncData.getReadItemsIds(), token);
readItemsCompletable = setItemsReadState(true, syncData.getReadIds(), token);
}
Completable unreadItemsCompletable;
if (syncData.getUnreadItemsIds().isEmpty()) {
if (syncData.getUnreadIds().isEmpty()) {
unreadItemsCompletable = Completable.complete();
} else {
unreadItemsCompletable = setItemsReadState(false, syncData.getUnreadItemsIds(), token);
unreadItemsCompletable = setItemsReadState(false, syncData.getUnreadIds(), token);
}
return readItemsCompletable.concatWith(unreadItemsCompletable);
@ -298,17 +298,17 @@ public class FreshRSSDataSource {
*/
private Completable setItemsStarState(@NonNull FreshRSSSyncData syncData, @NonNull String token) {
Completable starredItemsCompletable;
if (syncData.getStarredItemsIds().isEmpty()) {
if (syncData.getStarredIds().isEmpty()) {
starredItemsCompletable = Completable.complete();
} else {
starredItemsCompletable = setItemsStarState(true, syncData.getStarredItemsIds(), token);
starredItemsCompletable = setItemsStarState(true, syncData.getStarredIds(), token);
}
Completable unstarredItemsCompletable;
if (syncData.getUnstarredItemsIds().isEmpty()) {
if (syncData.getUnstarredIds().isEmpty()) {
unstarredItemsCompletable = Completable.complete();
} else {
unstarredItemsCompletable = setItemsStarState(false, syncData.getUnstarredItemsIds(), token);
unstarredItemsCompletable = setItemsStarState(false, syncData.getUnstarredIds(), token);
}
return starredItemsCompletable.concatWith(unstarredItemsCompletable);

View File

@ -2,8 +2,8 @@ package com.readrops.api.services.freshrss
data class FreshRSSSyncData(
var lastModified: Long = 0,
var readItemsIds: List<String> = listOf(),
var unreadItemsIds: List<String> = listOf(),
var starredItemsIds: List<String> = listOf(),
var unstarredItemsIds: List<String> = listOf(),
var readIds: List<String> = listOf(),
var unreadIds: List<String> = listOf(),
var starredIds: List<String> = listOf(),
var unstarredIds: List<String> = listOf(),
)

View File

@ -121,22 +121,22 @@ class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
}
private suspend fun setItemsReadState(syncData: FreshRSSSyncData, token: String) {
if (syncData.readItemsIds.isNotEmpty()) {
setItemsReadState(true, syncData.readItemsIds, token)
if (syncData.readIds.isNotEmpty()) {
setItemsReadState(true, syncData.readIds, token)
}
if (syncData.unreadItemsIds.isNotEmpty()) {
setItemsReadState(false, syncData.unreadItemsIds, token)
if (syncData.unreadIds.isNotEmpty()) {
setItemsReadState(false, syncData.unreadIds, token)
}
}
private suspend fun setItemsStarState(syncData: FreshRSSSyncData, token: String) {
if (syncData.starredItemsIds.isNotEmpty()) {
setItemStarState(true, syncData.starredItemsIds, token)
if (syncData.starredIds.isNotEmpty()) {
setItemStarState(true, syncData.starredIds, token)
}
if (syncData.unstarredItemsIds.isNotEmpty()) {
setItemStarState(false, syncData.unstarredItemsIds, token)
if (syncData.unstarredIds.isNotEmpty()) {
setItemStarState(false, syncData.unstarredIds, token)
}
}

View File

@ -96,22 +96,22 @@ public class FreshRSSRepository extends ARepository {
.itemStateChangesDao()
.getItemStateChanges(account.getId());
syncData.setReadItemsIds(itemStateChanges.stream()
syncData.setReadIds(itemStateChanges.stream()
.filter(it -> it.getReadChange() && it.getRead())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setUnreadItemsIds(itemStateChanges.stream()
syncData.setUnreadIds(itemStateChanges.stream()
.filter(it -> it.getReadChange() && !it.getRead())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setStarredItemsIds(itemStateChanges.stream()
syncData.setStarredIds(itemStateChanges.stream()
.filter(it -> it.getStarChange() && it.getStarred())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));
syncData.setUnstarredItemsIds(itemStateChanges.stream()
syncData.setUnstarredIds(itemStateChanges.stream()
.filter(it -> it.getStarChange() && !it.getStarred())
.map(ItemReadStarState::getRemoteId)
.collect(Collectors.toList()));

View File

@ -91,4 +91,6 @@ dependencies {
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
coreLibraryDesugaring(libs.jdk.desugar)
implementation(libs.encrypted.preferences)
}

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name" translatable="false">ReadropsDebug</string>
</resources>

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".ReadropsApp"
android:allowBackup="true"

View File

@ -1,5 +1,7 @@
package com.readrops.app.compose
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.readrops.api.services.Credentials
import com.readrops.app.compose.account.AccountScreenModel
import com.readrops.app.compose.account.credentials.AccountCredentialsScreenModel
@ -13,6 +15,7 @@ import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineScreenModel
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.koin.android.ext.koin.androidContext
import org.koin.core.parameter.parametersOf
import org.koin.dsl.module
@ -44,4 +47,18 @@ val composeAppModule = module {
else -> throw IllegalArgumentException("Unknown account type")
}
}
single {
val masterKey = MasterKey.Builder(androidContext())
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
androidContext(),
"account_credentials",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}

View File

@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@ -36,8 +37,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.compose.R
import com.readrops.app.compose.home.HomeScreen
import com.readrops.app.compose.util.components.AndroidScreen
import com.readrops.app.compose.util.ErrorMessage
import com.readrops.app.compose.util.components.AndroidScreen
import com.readrops.app.compose.util.theme.ShortSpacer
import com.readrops.app.compose.util.theme.VeryLargeSpacer
import com.readrops.app.compose.util.theme.spacing
@ -118,7 +119,7 @@ class AccountCredentialsScreen(
label = { Text(text = stringResource(id = R.string.login)) },
singleLine = true,
isError = state.isLoginError,
supportingText = { Text(text = state.passwordError?.errorText().orEmpty()) },
supportingText = { Text(text = state.loginError?.errorText().orEmpty()) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
modifier = Modifier.fillMaxWidth()
)
@ -178,7 +179,7 @@ class AccountCredentialsScreen(
ShortSpacer()
Text(
text = ErrorMessage.get(exception = state.loginException!!),
text = ErrorMessage.get(state.loginException!!, LocalContext.current),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)

View File

@ -1,5 +1,6 @@
package com.readrops.app.compose.account.credentials
import android.content.SharedPreferences
import android.util.Patterns
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
@ -72,7 +73,13 @@ class AccountCredentialsScreenModel(
return@launch
}
database.newAccountDao().insert(account)
account.id = database.newAccountDao().insert(account).toInt()
get<SharedPreferences>().edit()
.putString(account.loginKey, account.login)
.putString(account.passwordKey, account.password)
.apply()
mutableState.update { it.copy(goToHomeScreen = true) }
}
}

View File

@ -1,7 +1,10 @@
package com.readrops.app.compose.base
import android.content.SharedPreferences
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.readrops.api.services.Credentials
import com.readrops.api.utils.AuthInterceptor
import com.readrops.app.compose.repositories.BaseRepository
import com.readrops.db.Database
import com.readrops.db.entities.account.Account
@ -35,8 +38,17 @@ abstract class TabScreenModel(
.distinctUntilChanged()
.collect { account ->
if (account != null) {
if (account.login == null || account.password == null) {
val encryptedPreferences = get<SharedPreferences>()
account.login = encryptedPreferences.getString(account.loginKey, null)
account.password = encryptedPreferences.getString(account.passwordKey, null)
}
currentAccount = account
repository = get(parameters = { parametersOf(account) })
// very important to avoid credentials conflicts between accounts
get<AuthInterceptor>().credentials = Credentials.toCredentials(account)
accountEvent.emit(account)
}

View File

@ -42,7 +42,19 @@ class FreshRSSRepository(
): Pair<SyncResult, ErrorResult> = throw NotImplementedError("This method can't be called here")
override suspend fun synchronize(): SyncResult {
val syncData = FreshRSSSyncData()
val itemStateChanges = database.newItemStateChangeDao()
.selectItemStateChanges(account.id)
val syncData = FreshRSSSyncData(
readIds = itemStateChanges.filter { it.readChange && it.read }
.map { it.remoteId },
unreadIds = itemStateChanges.filter { it.readChange && !it.read }
.map { it.remoteId },
starredIds = itemStateChanges.filter { it.starChange && it.starred }
.map { it.remoteId },
unstarredIds = itemStateChanges.filter { it.starChange && !it.starred }
.map { it.remoteId }
)
val syncType: SyncType
if (account.lastModified != 0L) {
@ -65,6 +77,8 @@ class FreshRSSRepository(
account.lastModified = newLastModified
database.newAccountDao().updateLastModified(newLastModified, account.id)
database.newItemStateChangeDao().resetStateChanges(account.id)
}
}

View File

@ -5,28 +5,26 @@ import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
import com.readrops.db.entities.ItemState
import com.readrops.db.entities.account.Account
typealias ErrorResult = Map<Feed, Exception>
abstract class ARepository(
val database: Database,
val account: Account
) {
interface Repository {
/**
* This method is intended for remote accounts.
*/
abstract suspend fun login(account: Account)
suspend fun login(account: Account)
/**
* Global synchronization for the local account.
* @param selectedFeeds feeds to be updated
* @param onUpdate get synchronization status
* @return returns the result of the synchronization used by notifications
* @param selectedFeeds feeds to be updated, will fetch all account feeds if list is empty
* @param onUpdate notify each feed update
* @return the result of the synchronization used by notifications
* and errors per feed if occurred to be transmitted to the user
*/
abstract suspend fun synchronize(
suspend fun synchronize(
selectedFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): Pair<SyncResult, ErrorResult>
@ -34,16 +32,23 @@ abstract class ARepository(
/**
* Global synchronization for remote accounts. Unlike the local account, remote accounts
* won't benefit from synchronization status and granular synchronization
* @return the result of the synchronization
*/
abstract suspend fun synchronize(): SyncResult
suspend fun synchronize(): SyncResult
abstract suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit): ErrorResult
/**
* Insert new feeds by notifying each of them
* @param newFeeds feeds to insert
* @param onUpdate notify each feed insertion
* @return errors by feed
*/
suspend fun insertNewFeeds(newFeeds: List<Feed>, onUpdate: (Feed) -> Unit): ErrorResult
}
abstract class BaseRepository(
database: Database,
account: Account,
) : ARepository(database, account) {
val database: Database,
val account: Account,
) : Repository {
open suspend fun updateFeed(feed: Feed) =
database.newFeedDao().updateFeedFields(feed.id, feed.name!!, feed.url!!, feed.folderId)
@ -57,11 +62,39 @@ abstract class BaseRepository(
open suspend fun deleteFolder(folder: Folder) = database.newFolderDao().delete(folder)
open suspend fun setItemReadState(item: Item) {
database.newItemDao().updateReadState(item.id, item.isRead)
// TODO handle Nextcloud News case
if (account.config.useSeparateState) {
database.newItemStateChangeDao().upsertItemReadStateChange(item, account.id, true)
database.newItemStateDao().upsertItemReadState(
ItemState(
id = 0,
read = item.isRead,
starred = item.isStarred,
remoteId = item.remoteId!!,
accountId = account.id
)
)
} else {
database.newItemDao().updateReadState(item.id, item.isRead)
}
}
open suspend fun setItemStarState(item: Item) {
database.newItemDao().updateStarState(item.id, item.isStarred)
// TODO handle Nextcloud News case
if (account.config.useSeparateState) {
database.newItemStateChangeDao().upsertItemStarStateChange(item, account.id, true)
database.newItemStateDao().upsertItemStarState(
ItemState(
id = 0,
read = item.isRead,
starred = item.isStarred,
remoteId = item.remoteId!!,
accountId = account.id
)
)
} else {
database.newItemDao().updateStarState(item.id, item.isStarred)
}
}
open suspend fun setAllItemsRead(accountId: Int) {

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@ -44,7 +45,7 @@ fun ErrorListDialog(
modifier = Modifier.verticalScroll(scrollableState)
) {
for (error in errorResult.entries) {
Text(text = "${error.key.name}: ${ErrorMessage.get(error.value)}")
Text(text = "${error.key.name}: ${ErrorMessage.get(error.value, LocalContext.current)}")
ShortSpacer()
}

View File

@ -2,8 +2,7 @@ package com.readrops.app.compose.timelime
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
@ -32,6 +31,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -68,7 +68,13 @@ class TimelineScreenModel(
database.newItemDao().selectAll(query)
},
).flow
.cachedIn(screenModelScope)
.transformLatest { value ->
if (!timelineState.value.isRefreshing) {
emit(value)
}
}
.cachedIn(screenModelScope),
isAccountLocal = account.isLocal
)
}
@ -101,11 +107,11 @@ class TimelineScreenModel(
} else {
_timelineState.update { it.copy(isRefreshing = true) }
repository?.synchronize()
try {
repository?.synchronize()
} catch (e: Exception) {
// handle sync exceptions
Log.d("TimelineScreenModel", "refreshTimeline: ${e.message}")
_timelineState.update { it.copy(syncError = e, isRefreshing = false) }
return@launch
}
_timelineState.update {
@ -159,7 +165,7 @@ class TimelineScreenModel(
it.copy(
isRefreshing = false,
endSynchronizing = true,
synchronizationErrors = if (results!!.second.isNotEmpty()) results.second else null
localSyncErrors = if (results!!.second.isNotEmpty()) results.second else null
)
}
}
@ -286,7 +292,7 @@ class TimelineScreenModel(
fun closeDialog(dialog: DialogState? = null) {
if (dialog is DialogState.ErrorList) {
_timelineState.update { it.copy(synchronizationErrors = null) }
_timelineState.update { it.copy(localSyncErrors = null) }
}
_timelineState.update { it.copy(dialog = null) }
@ -319,7 +325,7 @@ class TimelineScreenModel(
}
@Immutable
@Stable
data class TimelineState(
val isRefreshing: Boolean = false,
val isDrawerOpen: Boolean = false,
@ -328,16 +334,20 @@ data class TimelineState(
val feedCount: Int = 0,
val feedMax: Int = 0,
val endSynchronizing: Boolean = false,
val synchronizationErrors: ErrorResult? = null,
val localSyncErrors: ErrorResult? = null,
val syncError: Exception? = null,
val filters: QueryFilters = QueryFilters(),
val filterFeedName: String = "",
val filterFolderName: String = "",
val foldersAndFeeds: Map<Folder?, List<Feed>> = emptyMap(),
val itemState: Flow<PagingData<ItemWithFeed>> = emptyFlow(),
val dialog: DialogState? = null
val dialog: DialogState? = null,
val isAccountLocal: Boolean = false
) {
val showSubtitle = filters.subFilter != SubFilter.ALL
val displayRefreshScreen = isRefreshing && isAccountLocal
}
sealed interface DialogState {

View File

@ -52,6 +52,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
import com.readrops.app.compose.R
import com.readrops.app.compose.item.ItemScreen
import com.readrops.app.compose.timelime.drawer.TimelineDrawer
import com.readrops.app.compose.util.ErrorMessage
import com.readrops.app.compose.util.components.CenteredProgressIndicator
import com.readrops.app.compose.util.components.Placeholder
import com.readrops.app.compose.util.components.RefreshScreen
@ -127,25 +128,31 @@ object TimelineTab : Tab {
}
}
LaunchedEffect(state.synchronizationErrors) {
if (state.synchronizationErrors != null) {
LaunchedEffect(state.localSyncErrors) {
if (state.localSyncErrors != null) {
val action = snackbarHostState.showSnackbar(
message = context.resources.getQuantityString(
R.plurals.error_occurred,
state.synchronizationErrors!!.size
state.localSyncErrors!!.size
),
actionLabel = context.getString(R.string.details),
duration = SnackbarDuration.Short
)
if (action == SnackbarResult.ActionPerformed) {
viewModel.openDialog(DialogState.ErrorList(state.synchronizationErrors!!))
viewModel.openDialog(DialogState.ErrorList(state.localSyncErrors!!))
} else {
// remove errors from state
viewModel.closeDialog(DialogState.ErrorList(state.synchronizationErrors!!))
viewModel.closeDialog(DialogState.ErrorList(state.localSyncErrors!!))
}
}
}
LaunchedEffect(state.syncError) {
if (state.syncError != null) {
snackbarHostState.showSnackbar(ErrorMessage.get(state.syncError!!, context))
}
}
when (val dialog = state.dialog) {
is DialogState.ConfirmDialog -> {
@ -293,7 +300,7 @@ object TimelineTab : Tab {
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
when {
state.isRefreshing -> RefreshScreen(
state.displayRefreshScreen -> RefreshScreen(
currentFeed = state.currentFeed,
feedCount = state.feedCount,
feedMax = state.feedMax

View File

@ -1,7 +1,6 @@
package com.readrops.app.compose.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import android.content.Context
import com.readrops.api.utils.exceptions.HttpException
import com.readrops.api.utils.exceptions.ParseException
import com.readrops.api.utils.exceptions.UnknownFormatException
@ -11,33 +10,31 @@ import java.net.UnknownHostException
object ErrorMessage {
@Composable
fun get(exception: Exception) = when (exception) {
is HttpException -> getHttpMessage(exception)
is UnknownHostException -> stringResource(R.string.unreachable_url)
is NoSuchFileException -> stringResource(R.string.unable_open_file)
is IOException -> stringResource(R.string.network_failure, exception.message.orEmpty())
is ParseException, is UnknownFormatException -> stringResource(R.string.processing_feed_error)
fun get(exception: Exception, context: Context) = when (exception) {
is HttpException -> getHttpMessage(exception, context)
is UnknownHostException -> context.resources.getString(R.string.unreachable_url)
is NoSuchFileException -> context.resources.getString(R.string.unable_open_file)
is IOException -> context.resources.getString(R.string.network_failure, exception.message.orEmpty())
is ParseException, is UnknownFormatException -> context.resources.getString(R.string.processing_feed_error)
else -> "${exception.javaClass.simpleName}: ${exception.message}"
}
@Composable
private fun getHttpMessage(exception: HttpException): String {
private fun getHttpMessage(exception: HttpException, context: Context): String {
return when (exception.code) {
in 400..499 -> {
when (exception.code) {
400 -> stringResource(id = R.string.http_error_400)
401 -> stringResource(id = R.string.http_error_401)
403 -> stringResource(id = R.string.http_error_403)
404 -> stringResource(id = R.string.http_error_404)
else -> stringResource(id = R.string.http_error_4XX, exception.code)
400 -> context.resources.getString(R.string.http_error_400)
401 -> context.resources.getString(R.string.http_error_401)
403 -> context.resources.getString(R.string.http_error_403)
404 -> context.resources.getString(R.string.http_error_404)
else -> context.resources.getString(R.string.http_error_4XX, exception.code)
}
}
in 500..599 -> {
stringResource(id = R.string.http_error_5XX, exception.code)
context.resources.getString(R.string.http_error_5XX, exception.code)
}
else -> stringResource(id = R.string.http_error, exception.code)
else -> context.resources.getString(R.string.http_error, exception.code)
}
}
}

View File

@ -2,6 +2,7 @@ package com.readrops.app.compose.util.components
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.readrops.app.compose.R
@ -17,7 +18,7 @@ fun ErrorDialog(
icon = painterResource(id = R.drawable.ic_error),
onDismiss = onDismiss
) {
Text(text = ErrorMessage.get(exception = exception))
Text(text = ErrorMessage.get(exception, LocalContext.current))
}
}

View File

@ -161,7 +161,7 @@
<string name="http_error_401">Erreur HTTP 401, veuillez vérifier vos identifiants</string>
<string name="http_error_403">Erreur HTTP 403, accès interdit</string>
<string name="http_error_404">Erreur HTTP 404, l\'URL n\'existe pas</string>
<string name="http_error_4XX">Erreur HTTP %1$s, veuillez vérifier vos champs</string>
<string name="http_error_5XX">Erreur HTTP %1$s, erreur serveur</string>
<string name="http_error">Erreur HTTP %1$s</string>
<string name="http_error_4XX">Erreur HTTP %1$d, veuillez vérifier vos champs</string>
<string name="http_error_5XX">Erreur HTTP %1$d, erreur serveur</string>
<string name="http_error">Erreur HTTP %1$d</string>
</resources>

View File

@ -167,7 +167,7 @@
<string name="http_error_401">HTTP error 401, please check your credentials</string>
<string name="http_error_403">HTTP error 403, access forbidden</string>
<string name="http_error_404">HTTP error 404, URL not found</string>
<string name="http_error_4XX">HTTP error %1$s, please check your fields</string>
<string name="http_error_5XX">HTTP error %1$s, server error</string>
<string name="http_error">HTTP error %1$s</string>
<string name="http_error_4XX">HTTP error %1$d, please check your fields</string>
<string name="http_error_5XX">HTTP error %1$d, server error</string>
<string name="http_error">HTTP error %1$d</string>
</resources>

View File

@ -17,13 +17,13 @@ interface NewItemStateChangeDao: NewBaseDao<ItemStateChange> {
"ItemStateChange.read_change, ItemStateChange.star_change, Item.remoteId " +
"From ItemStateChange Inner Join Item On ItemStateChange.id = Item.id " +
"Left Join ItemState On ItemState.remote_id = Item.remoteId Where ItemStateChange.account_id = :accountId")
suspend fun getItemStateChanges(accountId: Int): List<ItemReadStarState>
suspend fun selectItemStateChanges(accountId: Int): List<ItemReadStarState>
@Query("Select Item.read, Item.starred," +
"ItemStateChange.read_change, ItemStateChange.star_change, Item.remoteId " +
"From ItemStateChange Inner Join Item On ItemStateChange.id = Item.id " +
"Where ItemStateChange.account_id = :accountId")
suspend fun getNextcloudNewsStateChanges(accountId: Int): List<ItemReadStarState>
suspend fun selectNextcloudNewsStateChanges(accountId: Int): List<ItemReadStarState>
@Query("Select Case When :itemId In (Select id From ItemStateChange Where read_change = 1) Then 1 Else 0 End")
suspend fun readStateChangeExists(itemId: Int): Boolean
@ -34,9 +34,9 @@ interface NewItemStateChangeDao: NewBaseDao<ItemStateChange> {
suspend fun upsertItemReadStateChange(item: Item, accountId: Int, useSeparateState: Boolean) {
if (itemStateChangeExists(item.id, accountId)) {
val oldItemReadState = if (useSeparateState)
getItemReadState(item.remoteId!!, accountId)
selectItemReadState(item.remoteId!!, accountId)
else
getStandardItemReadState(item.remoteId!!, accountId)
selectStandardItemReadState(item.remoteId!!, accountId)
val readChange = item.isRead != oldItemReadState
@ -58,9 +58,9 @@ interface NewItemStateChangeDao: NewBaseDao<ItemStateChange> {
suspend fun upsertItemStarStateChange(item: Item, accountId: Int, useSeparateState: Boolean) {
if (itemStateChangeExists(item.id, accountId)) {
val oldItemStarState = if (useSeparateState)
getItemStarState(item.remoteId!!, accountId)
selectItemStarState(item.remoteId!!, accountId)
else
getStandardItemStarState(item.remoteId!!, accountId)
selectStandardItemStarState(item.remoteId!!, accountId)
val starChange = item.isStarred != oldItemStarState
@ -86,16 +86,16 @@ interface NewItemStateChangeDao: NewBaseDao<ItemStateChange> {
fun itemStateChangeExists(id: Int, accountId: Int): Boolean
@Query("Select read From ItemState Where remote_id = :remoteId And account_id = :accountId")
fun getItemReadState(remoteId: String, accountId: Int): Boolean
fun selectItemReadState(remoteId: String, accountId: Int): Boolean
@Query("Select read From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId = :remoteId And account_id = :accountId")
fun getStandardItemReadState(remoteId: String, accountId: Int): Boolean
fun selectStandardItemReadState(remoteId: String, accountId: Int): Boolean
@Query("Select starred From ItemState Where remote_id = :remoteId And account_id = :accountId")
fun getItemStarState(remoteId: String, accountId: Int): Boolean
fun selectItemStarState(remoteId: String, accountId: Int): Boolean
@Query("Select starred From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId = :remoteId And account_id = :accountId")
fun getStandardItemStarState(remoteId: String, accountId: Int): Boolean
fun selectStandardItemStarState(remoteId: String, accountId: Int): Boolean
@Query("Update ItemStateChange set read_change = :readChange Where id = :id")
fun updateItemReadStateChange(readChange: Boolean, id: Int)

View File

@ -65,6 +65,8 @@ kotlinxmlbuilder = "org.redundent:kotlin-xml-builder:1.7.3" #TODO update this
jdk-desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
encrypted-preferences = "androidx.security:security-crypto:1.1.0-alpha06"
[bundles]
compose = ["bom", "compose-foundation", "compose-runtime", "compose-animation",
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3"]