mirror of https://github.com/readrops/Readrops.git
Compare commits
13 Commits
d6ede0d2ce
...
6bfc8ad2ac
Author | SHA1 | Date |
---|---|---|
Alexandre Alapetite | 6bfc8ad2ac | |
Shinokuni | ffe2d5cffb | |
Shinokuni | 19409f20a5 | |
Shinokuni | d79c5b4637 | |
Shinokuni | d19b8d90d4 | |
Shinokuni | f9a2eb5e2c | |
Shinokuni | 0fbaec1263 | |
Shinokuni | b3c252a434 | |
Shinokuni | fbe14121b0 | |
Shinokuni | df2ad13872 | |
Alexandre Alapetite | 9c73738cab | |
Alexandre Alapetite | d5d8b16148 | |
Alexandre Alapetite | 7e5c0268ea |
|
@ -9,8 +9,8 @@ data class SyncResult(
|
|||
var starredItems: List<Item> = mutableListOf(),
|
||||
var feeds: List<Feed> = listOf(),
|
||||
var folders: List<Folder> = listOf(),
|
||||
var unreadIds: List<String>? = null,
|
||||
var readIds: List<String>? = null,
|
||||
var starredIds: List<String>? = null,
|
||||
var unreadIds: List<String> = listOf(),
|
||||
var readIds: List<String> = listOf(),
|
||||
var starredIds: List<String> = listOf(),
|
||||
var isError: Boolean = false
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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 lastModified: Long = 0,
|
||||
var readIds: List<String> = listOf(),
|
||||
var unreadIds: List<String> = listOf(),
|
||||
var starredIds: List<String> = listOf(),
|
||||
var unstarredIds: List<String> = listOf(),
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
package com.readrops.api.services.freshrss
|
||||
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
|
||||
import com.readrops.db.entities.Item
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -14,10 +15,10 @@ class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
|
|||
|
||||
suspend fun login(login: String, password: String): String {
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Email", login)
|
||||
.addFormDataPart("Passwd", password)
|
||||
.build()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Email", login)
|
||||
.addFormDataPart("Passwd", password)
|
||||
.build()
|
||||
|
||||
val response = service.login(requestBody)
|
||||
|
||||
|
@ -32,25 +33,50 @@ class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
|
|||
|
||||
suspend fun getUserInfo(): FreshRSSUserInfo = service.userInfo()
|
||||
|
||||
suspend fun sync(): SyncResult = with(CoroutineScope(Dispatchers.IO)) {
|
||||
return SyncResult().apply {
|
||||
folders = async { getFolders() }.await()
|
||||
feeds = async { getFeeds() }.await()
|
||||
//items = async { getItems(listOf(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null) }.await()
|
||||
suspend fun synchronize(
|
||||
syncType: SyncType,
|
||||
syncData: FreshRSSSyncData,
|
||||
writeToken: String
|
||||
): SyncResult = with(CoroutineScope(Dispatchers.IO)) {
|
||||
return if (syncType == SyncType.INITIAL_SYNC) {
|
||||
SyncResult().apply {
|
||||
folders = async { getFolders() }.await()
|
||||
feeds = async { getFeeds() }.await()
|
||||
items =
|
||||
async { getItems(listOf(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null) }.await()
|
||||
starredItems = async { getStarredItems(MAX_STARRED_ITEMS) }.await()
|
||||
unreadIds =
|
||||
async { getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS) }.await()
|
||||
starredIds = async { getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS) }.await()
|
||||
}
|
||||
} else {
|
||||
SyncResult().apply {
|
||||
setItemsReadState(syncData, writeToken)
|
||||
setItemsStarState(syncData, writeToken)
|
||||
|
||||
folders = async { getFolders() }.await()
|
||||
feeds = async { getFeeds() }.await()
|
||||
items = async { getItems(null, MAX_ITEMS, syncData.lastModified) }.await()
|
||||
|
||||
unreadIds = async { getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS) }.await()
|
||||
readIds = async { getItemsIds(GOOGLE_UNREAD, GOOGLE_READING_LIST, MAX_ITEMS) }.await()
|
||||
starredIds = async { getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS) }.await()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suspend fun getFolders() = service.getFolders()
|
||||
|
||||
suspend fun getFeeds() = service.getFeeds()
|
||||
|
||||
suspend fun getItems(excludeTargets: List<String>, max: Int, lastModified: Long?): List<Item> {
|
||||
suspend fun getItems(excludeTargets: List<String>?, max: Int, lastModified: Long?): List<Item> {
|
||||
return service.getItems(excludeTargets, max, lastModified)
|
||||
}
|
||||
|
||||
suspend fun getStarredItems(max: Int) = service.getStarredItems(max)
|
||||
|
||||
suspend fun getItemsIds(excludeTarget: String, includeTarget: String, max: Int): List<String> {
|
||||
suspend fun getItemsIds(excludeTarget: String?, includeTarget: String, max: Int): List<String> {
|
||||
return service.getItemsIds(excludeTarget, includeTarget, max)
|
||||
}
|
||||
|
||||
|
@ -71,7 +97,7 @@ class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
|
|||
}
|
||||
|
||||
suspend fun createFeed(token: String, feedUrl: String) {
|
||||
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe");
|
||||
service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe")
|
||||
}
|
||||
|
||||
suspend fun deleteFeed(token: String, feedUrl: String) {
|
||||
|
@ -94,23 +120,23 @@ class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
|
|||
service.deleteFolder(token, folderId)
|
||||
}
|
||||
|
||||
suspend fun setItemsReadState(syncData: FreshRSSSyncData, token: String) {
|
||||
if (syncData.readItemsIds.isNotEmpty()) {
|
||||
setItemsReadState(true, syncData.readItemsIds, token)
|
||||
private suspend fun setItemsReadState(syncData: FreshRSSSyncData, token: String) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setItemsStarState(syncData: FreshRSSSyncData, token: String) {
|
||||
if (syncData.starredItemsIds.isNotEmpty()) {
|
||||
setItemStarState(true, syncData.starredItemsIds, token)
|
||||
private suspend fun setItemsStarState(syncData: FreshRSSSyncData, token: String) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.readrops.api.services.freshrss.adapters
|
||||
|
||||
import android.util.TimingLogger
|
||||
import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_READ
|
||||
import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_STARRED
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
|
@ -89,9 +88,11 @@ class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
|
|||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
|
||||
when (reader.nextName()) {
|
||||
"href" -> href = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"href" -> href = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
|
@ -108,7 +109,6 @@ class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
|
|||
when (reader.nextString()) {
|
||||
GOOGLE_READ -> item.isRead = true
|
||||
GOOGLE_STARRED -> item.isStarred = true
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,10 @@
|
|||
],
|
||||
"categories": [
|
||||
"user/-/state/com.google/reading-list",
|
||||
"user/-/label/Libre"
|
||||
"user/-/label/Libre",
|
||||
"category1",
|
||||
"category2",
|
||||
"category3"
|
||||
],
|
||||
"origin": {
|
||||
"streamId": "feed/15",
|
||||
|
@ -44,7 +47,10 @@
|
|||
"user/-/state/com.google/reading-list",
|
||||
"user/-/label/Libre",
|
||||
"user/-/state/com.google/starred",
|
||||
"user/-/state/com.google/read"
|
||||
"user/-/state/com.google/read",
|
||||
"category1",
|
||||
"category2",
|
||||
"category3"
|
||||
],
|
||||
"origin": {
|
||||
"streamId": "feed/15",
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -91,4 +91,6 @@ dependencies {
|
|||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
|
||||
|
||||
coreLibraryDesugaring(libs.jdk.desugar)
|
||||
|
||||
implementation(libs.encrypted.preferences)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">ReadropsDebug</string>
|
||||
</resources>
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -2,13 +2,18 @@ package com.readrops.app.compose.repositories
|
|||
|
||||
import com.readrops.api.services.Credentials
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.api.services.SyncType
|
||||
import com.readrops.api.services.freshrss.FreshRSSSyncData
|
||||
import com.readrops.api.services.freshrss.NewFreshRSSDataSource
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.app.compose.util.Utils
|
||||
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
|
||||
import org.joda.time.DateTime
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
class FreshRSSRepository(
|
||||
|
@ -21,11 +26,12 @@ class FreshRSSRepository(
|
|||
val authInterceptor = getKoin().get<AuthInterceptor>()
|
||||
authInterceptor.credentials = Credentials.toCredentials(account)
|
||||
|
||||
val authToken = dataSource.login(account.login!!, account.password!!)
|
||||
account.token = authToken
|
||||
account.token = dataSource.login(account.login!!, account.password!!)
|
||||
// we got the authToken, time to provide it to make real calls
|
||||
authInterceptor.credentials = Credentials.toCredentials(account)
|
||||
|
||||
account.writeToken = dataSource.getWriteToken()
|
||||
|
||||
val userInfo = dataSource.getUserInfo()
|
||||
account.displayedName = userInfo.userName
|
||||
}
|
||||
|
@ -33,19 +39,47 @@ class FreshRSSRepository(
|
|||
override suspend fun synchronize(
|
||||
selectedFeeds: List<Feed>,
|
||||
onUpdate: (Feed) -> Unit
|
||||
): Pair<SyncResult, ErrorResult> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
): Pair<SyncResult, ErrorResult> = throw NotImplementedError("This method can't be called here")
|
||||
|
||||
override suspend fun synchronize(): SyncResult {
|
||||
val syncResult = dataSource.sync().apply {
|
||||
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) {
|
||||
syncType = SyncType.CLASSIC_SYNC
|
||||
syncData.lastModified = account.lastModified
|
||||
} else {
|
||||
syncType = SyncType.INITIAL_SYNC
|
||||
}
|
||||
|
||||
val newLastModified = DateTime.now().millis / 1000L
|
||||
|
||||
return dataSource.synchronize(syncType, syncData, account.writeToken!!).apply {
|
||||
insertFolders(folders)
|
||||
insertFeeds(feeds)
|
||||
|
||||
//insertItems(items)
|
||||
}
|
||||
insertItems(items, false)
|
||||
insertItems(starredItems, true)
|
||||
|
||||
return syncResult
|
||||
insertItemsIds(unreadIds, readIds, starredIds.toMutableList())
|
||||
|
||||
account.lastModified = newLastModified
|
||||
database.newAccountDao().updateLastModified(newLastModified, account.id)
|
||||
|
||||
database.newItemStateChangeDao().resetStateChanges(account.id)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insertNewFeeds(
|
||||
|
@ -65,7 +99,92 @@ class FreshRSSRepository(
|
|||
database.newFolderDao().upsertFolders(folders, account)
|
||||
}
|
||||
|
||||
private suspend fun insertItems(items: List<Item>) {
|
||||
private suspend fun insertItems(items: List<Item>, starredItems: Boolean) {
|
||||
val itemsToInsert = arrayListOf<Item>()
|
||||
val itemsFeedsIds = mutableMapOf<String?, Int>()
|
||||
|
||||
for (item in items) {
|
||||
val feedId: Int
|
||||
if (itemsFeedsIds.containsKey(item.feedRemoteId)) {
|
||||
feedId = itemsFeedsIds.getValue(item.feedRemoteId)
|
||||
} else {
|
||||
feedId =
|
||||
database.newFeedDao().selectRemoteFeedLocalId(item.feedRemoteId!!, account.id)
|
||||
itemsFeedsIds[item.feedRemoteId] = feedId
|
||||
}
|
||||
|
||||
item.feedId = feedId
|
||||
|
||||
if (item.text != null) {
|
||||
item.readTime = Utils.readTimeFromString(item.text!!)
|
||||
}
|
||||
|
||||
// workaround to avoid inserting starred items coming from the main item call
|
||||
// as the API exclusion filter doesn't seem to work
|
||||
if (!starredItems) {
|
||||
if (!item.isStarred) {
|
||||
itemsToInsert.add(item)
|
||||
}
|
||||
} else {
|
||||
itemsToInsert.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsToInsert.isNotEmpty()) {
|
||||
itemsToInsert.sortWith(Item::compareTo)
|
||||
database.itemDao().insert(itemsToInsert)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun insertItemsIds(
|
||||
unreadIds: List<String>,
|
||||
readIds: List<String>,
|
||||
starredIds: MutableList<String> // TODO is it performance wise?
|
||||
) {
|
||||
database.newItemStateDao().deleteItemStates(account.id)
|
||||
|
||||
database.newItemStateDao().insert(unreadIds.map { id ->
|
||||
val starred = starredIds.count { starredId -> starredId == id } == 1
|
||||
|
||||
if (starred) {
|
||||
starredIds.remove(id)
|
||||
}
|
||||
|
||||
ItemState(
|
||||
id = 0,
|
||||
read = false,
|
||||
starred = starred,
|
||||
remoteId = id,
|
||||
accountId = account.id
|
||||
)
|
||||
})
|
||||
|
||||
database.newItemStateDao().insert(readIds.map { id ->
|
||||
val starred = starredIds.count { starredId -> starredId == id } == 1
|
||||
if (starred) {
|
||||
starredIds.remove(id)
|
||||
}
|
||||
|
||||
ItemState(
|
||||
id = 0,
|
||||
read = true,
|
||||
starred = starred,
|
||||
remoteId = id,
|
||||
accountId = account.id
|
||||
)
|
||||
})
|
||||
|
||||
// insert starred items ids which are read
|
||||
if (starredIds.isNotEmpty()) {
|
||||
database.newItemStateDao().insert(starredIds.map { id ->
|
||||
ItemState(
|
||||
0,
|
||||
read = true,
|
||||
starred = true,
|
||||
remoteId = id,
|
||||
accountId = account.id
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.readrops.app.compose.timelime
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
|
@ -31,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
|
||||
|
||||
|
@ -54,7 +55,7 @@ class TimelineScreenModel(
|
|||
) { account, filters ->
|
||||
Pair(account, filters.copy(accountId = account.id))
|
||||
}.collectLatest { (account, filters) ->
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(filters)
|
||||
val query = ItemsQueryBuilder.buildItemsQuery(filters, account.config.useSeparateState)
|
||||
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
|
@ -67,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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -103,7 +110,8 @@ class TimelineScreenModel(
|
|||
try {
|
||||
repository?.synchronize()
|
||||
} catch (e: Exception) {
|
||||
// handle sync exceptions
|
||||
_timelineState.update { it.copy(syncError = e, isRefreshing = false) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
_timelineState.update {
|
||||
|
@ -157,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -284,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) }
|
||||
|
@ -314,10 +322,12 @@ class TimelineScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fun resetEndSynchronizing() {
|
||||
_timelineState.update { it.copy(endSynchronizing = false) }
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Stable
|
||||
data class TimelineState(
|
||||
val isRefreshing: Boolean = false,
|
||||
val isDrawerOpen: Boolean = false,
|
||||
|
@ -326,16 +336,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 {
|
||||
|
|
|
@ -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
|
||||
|
@ -101,6 +102,13 @@ object TimelineTab : Tab {
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.endSynchronizing) {
|
||||
if (state.endSynchronizing) {
|
||||
lazyListState.animateScrollToItem(0)
|
||||
viewModel.resetEndSynchronizing()
|
||||
}
|
||||
}
|
||||
|
||||
val drawerState = rememberDrawerState(
|
||||
initialValue = DrawerValue.Closed,
|
||||
confirmStateChange = {
|
||||
|
@ -127,26 +135,32 @@ 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 -> {
|
||||
TwoChoicesDialog(
|
||||
|
@ -293,7 +307,7 @@ object TimelineTab : Tab {
|
|||
.nestedScroll(pullToRefreshState.nestedScrollConnection)
|
||||
) {
|
||||
when {
|
||||
state.isRefreshing -> RefreshScreen(
|
||||
state.displayRefreshScreen -> RefreshScreen(
|
||||
currentFeed = state.currentFeed,
|
||||
feedCount = state.feedCount,
|
||||
feedMax = state.feedMax
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -3,12 +3,23 @@ package com.readrops.db
|
|||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.readrops.db.dao.*
|
||||
import com.readrops.db.dao.AccountDao
|
||||
import com.readrops.db.dao.FeedDao
|
||||
import com.readrops.db.dao.FolderDao
|
||||
import com.readrops.db.dao.ItemDao
|
||||
import com.readrops.db.dao.ItemStateChangeDao
|
||||
import com.readrops.db.dao.ItemStateDao
|
||||
import com.readrops.db.dao.newdao.NewAccountDao
|
||||
import com.readrops.db.dao.newdao.NewFeedDao
|
||||
import com.readrops.db.dao.newdao.NewFolderDao
|
||||
import com.readrops.db.dao.newdao.NewItemDao
|
||||
import com.readrops.db.entities.*
|
||||
import com.readrops.db.dao.newdao.NewItemStateChangeDao
|
||||
import com.readrops.db.dao.newdao.NewItemStateDao
|
||||
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.ItemStateChange
|
||||
import com.readrops.db.entities.account.Account
|
||||
import dev.matrix.roomigrant.GenerateRoomMigrations
|
||||
|
||||
|
@ -38,4 +49,8 @@ abstract class Database : RoomDatabase() {
|
|||
abstract fun newAccountDao(): NewAccountDao
|
||||
|
||||
abstract fun newFolderDao(): NewFolderDao
|
||||
|
||||
abstract fun newItemStateDao(): NewItemStateDao
|
||||
|
||||
abstract fun newItemStateChangeDao(): NewItemStateChangeDao
|
||||
}
|
|
@ -19,4 +19,7 @@ interface NewAccountDao : NewBaseDao<Account> {
|
|||
|
||||
@Query("Delete From Account")
|
||||
suspend fun deleteAllAccounts()
|
||||
|
||||
@Query("Update Account set last_modified = :lastModified Where id = :accountId")
|
||||
suspend fun updateLastModified(lastModified: Long, accountId: Int)
|
||||
}
|
|
@ -44,6 +44,9 @@ abstract class NewFeedDao : NewBaseDao<Feed> {
|
|||
@Query("Select id From Folder Where remoteId = :remoteId And account_id = :accountId")
|
||||
abstract suspend fun selectRemoteFolderLocalId(remoteId: String, accountId: Int): Int
|
||||
|
||||
@Query("Select id From Feed Where remoteId = :remoteId And account_id = :accountId")
|
||||
abstract suspend fun selectRemoteFeedLocalId(remoteId: String, accountId: Int): Int
|
||||
|
||||
@Query("Update Feed set name = :name, folder_id = :folderId Where remoteId = :remoteFeedId And account_id = :accountId")
|
||||
abstract fun updateFeedNameAndFolder(remoteFeedId: String, accountId: Int, name: String, folderId: Int?)
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package com.readrops.db.dao.newdao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.ItemStateChange
|
||||
import com.readrops.db.pojo.ItemReadStarState
|
||||
|
||||
@Dao
|
||||
interface NewItemStateChangeDao: NewBaseDao<ItemStateChange> {
|
||||
|
||||
@Query("Delete From ItemStateChange Where account_id = :accountId")
|
||||
suspend fun resetStateChanges(accountId: Int)
|
||||
|
||||
@Query("Select case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read, " +
|
||||
"case When ItemState.remote_id is NULL Or ItemState.starred = 1 Then 1 else 0 End starred," +
|
||||
"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 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 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
|
||||
|
||||
@Query("Select Case When :itemId In (Select id From ItemStateChange Where star_change = 1) Then 1 Else 0 End")
|
||||
suspend fun starStateChangeExists(itemId: Int): Boolean
|
||||
|
||||
suspend fun upsertItemReadStateChange(item: Item, accountId: Int, useSeparateState: Boolean) {
|
||||
if (itemStateChangeExists(item.id, accountId)) {
|
||||
val oldItemReadState = if (useSeparateState)
|
||||
selectItemReadState(item.remoteId!!, accountId)
|
||||
else
|
||||
selectStandardItemReadState(item.remoteId!!, accountId)
|
||||
|
||||
val readChange = item.isRead != oldItemReadState
|
||||
|
||||
if (readChange) {
|
||||
val oldItemStateChange = selectItemStateChange(item.id)
|
||||
val newReadChange = !oldItemStateChange.readChange
|
||||
|
||||
if (!newReadChange && !oldItemStateChange.starChange) {
|
||||
delete(oldItemStateChange)
|
||||
} else {
|
||||
updateItemReadStateChange(newReadChange, oldItemStateChange.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insert(ItemStateChange(id = item.id, readChange = true, accountId = accountId))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertItemStarStateChange(item: Item, accountId: Int, useSeparateState: Boolean) {
|
||||
if (itemStateChangeExists(item.id, accountId)) {
|
||||
val oldItemStarState = if (useSeparateState)
|
||||
selectItemStarState(item.remoteId!!, accountId)
|
||||
else
|
||||
selectStandardItemStarState(item.remoteId!!, accountId)
|
||||
|
||||
val starChange = item.isStarred != oldItemStarState
|
||||
|
||||
if (starChange) {
|
||||
val oldItemStateChange = selectItemStateChange(item.id)
|
||||
val newStarChange = !oldItemStateChange.starChange
|
||||
|
||||
if (!newStarChange && !oldItemStateChange.readChange) {
|
||||
delete(oldItemStateChange)
|
||||
} else {
|
||||
updateItemStarStateChange(newStarChange, oldItemStateChange.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insert(ItemStateChange(id = item.id, starChange = true, accountId = accountId))
|
||||
}
|
||||
}
|
||||
|
||||
@Query("Select * From ItemStateChange Where id = :id")
|
||||
fun selectItemStateChange(id: Int): ItemStateChange
|
||||
|
||||
@Query("Select case When Exists (Select id, account_id From ItemStateChange Where id = :id And account_id = :accountId) Then 1 else 0 End")
|
||||
fun itemStateChangeExists(id: Int, accountId: Int): Boolean
|
||||
|
||||
@Query("Select read From ItemState Where remote_id = :remoteId And account_id = :accountId")
|
||||
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 selectStandardItemReadState(remoteId: String, accountId: Int): Boolean
|
||||
|
||||
@Query("Select starred From ItemState Where remote_id = :remoteId And account_id = :accountId")
|
||||
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 selectStandardItemStarState(remoteId: String, accountId: Int): Boolean
|
||||
|
||||
@Query("Update ItemStateChange set read_change = :readChange Where id = :id")
|
||||
fun updateItemReadStateChange(readChange: Boolean, id: Int)
|
||||
|
||||
@Query("Update ItemStateChange set star_change = :starChange Where id = :id")
|
||||
fun updateItemStarStateChange(starChange: Boolean, id: Int)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.readrops.db.dao.newdao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.readrops.db.entities.ItemState
|
||||
|
||||
@Dao
|
||||
interface NewItemStateDao : NewBaseDao<ItemState> {
|
||||
|
||||
@Query("Delete From ItemState Where account_id = :accountId")
|
||||
suspend fun deleteItemStates(accountId: Int)
|
||||
|
||||
@Query("Update ItemState set read = :read Where remote_id = :remoteId And account_id = :accountId")
|
||||
suspend fun updateItemReadState(read: Boolean, remoteId: String, accountId: Int)
|
||||
|
||||
@Query("Update ItemState set starred = :star Where remote_id = :remoteId And account_id = :accountId")
|
||||
suspend fun updateItemStarState(star: Boolean, remoteId: String, accountId: Int)
|
||||
|
||||
@Query("Select case When Exists (Select remote_id, account_id From ItemState Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End")
|
||||
suspend fun itemStateExists(remoteId: String, accountId: Int): Boolean
|
||||
|
||||
suspend fun upsertItemReadState(itemState: ItemState) {
|
||||
if (itemStateExists(itemState.remoteId, itemState.accountId)) {
|
||||
updateItemReadState(itemState.read, itemState.remoteId, itemState.accountId)
|
||||
} else {
|
||||
insert(itemState)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertItemStarState(itemState: ItemState) {
|
||||
if (itemStateExists(itemState.remoteId, itemState.accountId)) {
|
||||
updateItemStarState(itemState.starred, itemState.remoteId, itemState.accountId)
|
||||
} else {
|
||||
insert(itemState)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
|
|
Loading…
Reference in New Issue