feat: added triggers for syncing.

Experimental for now, but works fine so far, I don't know about google api limit but I think it's pretty generous.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh 2023-12-20 22:12:46 +11:00
parent cc5e14088c
commit 178b00280b
No known key found for this signature in database
GPG Key ID: 14D73B142042BBA9
6 changed files with 181 additions and 1 deletions

View File

@ -34,6 +34,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.screen.data.SyncOptionsScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.presentation.util.relativeTimeSpanString
@ -559,6 +560,7 @@ private fun getSyncNowPref(): Preference.PreferenceGroup {
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_sync_now_group_title), title = stringResource(MR.strings.pref_sync_now_group_title),
preferenceItems = listOf( preferenceItems = listOf(
getSyncOptionsPref(),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_sync_now), title = stringResource(MR.strings.pref_sync_now),
subtitle = stringResource(MR.strings.pref_sync_now_subtitle), subtitle = stringResource(MR.strings.pref_sync_now_subtitle),
@ -570,6 +572,16 @@ private fun getSyncNowPref(): Preference.PreferenceGroup {
) )
} }
@Composable
private fun getSyncOptionsPref(): Preference.PreferenceItem.TextPreference {
val navigator = LocalNavigator.currentOrThrow
return Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_sync_options),
subtitle = stringResource(MR.strings.pref_sync_options_summ),
onClick = { navigator.push(SyncOptionsScreen()) },
)
}
@Composable @Composable
private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup { private fun getAutomaticSyncGroup(syncPreferences: SyncPreferences): Preference.PreferenceGroup {
val context = LocalContext.current val context = LocalContext.current

View File

@ -0,0 +1,125 @@
package eu.kanade.presentation.more.settings.screen.data
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.minus
import kotlinx.collections.immutable.plus
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.flow.update
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.LabeledCheckbox
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import androidx.compose.runtime.getValue
import tachiyomi.domain.sync.SyncPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SyncOptionsScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val model = rememberScreenModel { SyncOptionsScreenModel() }
val state by model.state.collectAsState()
Scaffold(
topBar = {
AppBar(
title = stringResource(MR.strings.pref_sync_options),
navigateUp = navigator::pop,
scrollBehavior = it,
)
},
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.fillMaxSize(),
) {
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = MaterialTheme.padding.medium),
) {
SyncChoices.forEach { (k, v) ->
item {
LabeledCheckbox(
label = stringResource(v),
checked = state.flags.contains(k),
onCheckedChange = {
model.toggleOptionFlag(k)
},
)
}
}
}
HorizontalDivider()
}
}
}
}
private class SyncOptionsScreenModel : StateScreenModel<SyncOptionsScreenModel.State>(State()) {
private val syncPreferences = Injekt.get<SyncPreferences>()
init {
loadInitialFlags()
}
private fun loadInitialFlags() {
val savedFlags = syncPreferences.syncFlags().get()
val flagSet = SyncPreferences.Flags.values().filter { flag ->
savedFlags and flag > 0
}.toSet().toPersistentSet()
mutableState.update { State(flags = flagSet) }
}
fun toggleOptionFlag(option: Int) {
mutableState.update { currentState ->
val newFlags = if (currentState.flags.contains(option)) {
currentState.flags - option
} else {
currentState.flags + option
}
saveFlags(newFlags)
currentState.copy(flags = newFlags)
}
}
private fun saveFlags(flags: PersistentSet<Int>) {
val flagsInt = flags.fold(0) { acc, flag -> acc or flag }
syncPreferences.syncFlags().set(flagsInt)
}
@Immutable
data class State(
val flags: PersistentSet<Int> = SyncChoices.keys.toPersistentSet(),
)
}
private val SyncChoices = mapOf(
SyncPreferences.Flags.SYNC_ON_CHAPTER_READ to MR.strings.sync_on_chapter_read,
SyncPreferences.Flags.SYNC_ON_CHAPTER_OPEN to MR.strings.sync_on_chapter_open,
SyncPreferences.Flags.SYNC_ON_APP_START to MR.strings.sync_on_app_start,
)

View File

@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer import eu.kanade.tachiyomi.data.coil.MangaKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.di.AppModule import eu.kanade.tachiyomi.di.AppModule
import eu.kanade.tachiyomi.di.PreferenceModule import eu.kanade.tachiyomi.di.PreferenceModule
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
@ -56,6 +57,7 @@ import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager import tachiyomi.presentation.widget.WidgetManager
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -168,6 +170,13 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
SecureActivityDelegate.onApplicationStart() SecureActivityDelegate.onApplicationStart()
val syncPreferences: SyncPreferences by injectLazy()
val syncFlags = syncPreferences.syncFlags().get()
if (syncPreferences.syncService().get() != 0 && syncFlags and SyncPreferences.Flags.SYNC_ON_APP_START == SyncPreferences.Flags.SYNC_ON_APP_START) {
SyncDataJob.startNow(this@App)
}
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.saver.Image import eu.kanade.tachiyomi.data.saver.Image
import eu.kanade.tachiyomi.data.saver.ImageSaver import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.saver.Location import eu.kanade.tachiyomi.data.saver.Location
import eu.kanade.tachiyomi.data.sync.SyncDataJob
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
@ -71,6 +72,7 @@ import tachiyomi.domain.history.model.HistoryUpdate
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.sync.SyncPreferences
import tachiyomi.source.local.isLocal import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -98,6 +100,7 @@ class ReaderViewModel @JvmOverloads constructor(
private val upsertHistory: UpsertHistory = Injekt.get(), private val upsertHistory: UpsertHistory = Injekt.get(),
private val updateChapter: UpdateChapter = Injekt.get(), private val updateChapter: UpdateChapter = Injekt.get(),
private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(), private val setMangaViewerFlags: SetMangaViewerFlags = Injekt.get(),
private val syncPreferences: SyncPreferences = Injekt.get()
) : ViewModel() { ) : ViewModel() {
private val mutableState = MutableStateFlow(State()) private val mutableState = MutableStateFlow(State())
@ -513,6 +516,7 @@ class ReaderViewModel @JvmOverloads constructor(
*/ */
private suspend fun updateChapterProgress(readerChapter: ReaderChapter, page: Page) { private suspend fun updateChapterProgress(readerChapter: ReaderChapter, page: Page) {
val pageIndex = page.index val pageIndex = page.index
val syncFlags = syncPreferences.syncFlags().get()
mutableState.update { mutableState.update {
it.copy(currentPage = pageIndex + 1) it.copy(currentPage = pageIndex + 1)
@ -527,6 +531,12 @@ class ReaderViewModel @JvmOverloads constructor(
readerChapter.chapter.read = true readerChapter.chapter.read = true
updateTrackChapterRead(readerChapter) updateTrackChapterRead(readerChapter)
deleteChapterIfNeeded(readerChapter) deleteChapterIfNeeded(readerChapter)
// Check if syncing is enabled for chapter read:
if (syncPreferences.syncService().get() != 0 &&
syncFlags and SyncPreferences.Flags.SYNC_ON_CHAPTER_READ == SyncPreferences.Flags.SYNC_ON_CHAPTER_READ) {
SyncDataJob.startNow(Injekt.get<Application>())
}
} }
updateChapter.await( updateChapter.await(
@ -536,6 +546,12 @@ class ReaderViewModel @JvmOverloads constructor(
lastPageRead = readerChapter.chapter.last_page_read.toLong(), lastPageRead = readerChapter.chapter.last_page_read.toLong(),
), ),
) )
// Check if syncing is enabled for chapter open:
if (syncPreferences.syncService().get() != 0 &&
syncFlags and SyncPreferences.Flags.SYNC_ON_CHAPTER_OPEN == SyncPreferences.Flags.SYNC_ON_CHAPTER_OPEN) {
SyncDataJob.startNow(Injekt.get<Application>())
}
} }
} }

View File

@ -6,6 +6,18 @@ import tachiyomi.core.preference.PreferenceStore
class SyncPreferences( class SyncPreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
object Flags {
const val NONE = 0x0
const val SYNC_ON_CHAPTER_READ = 0x1
const val SYNC_ON_CHAPTER_OPEN = 0x2
const val SYNC_ON_APP_START = 0x4
const val Defaults = NONE
fun values() = listOf(NONE, SYNC_ON_CHAPTER_READ, SYNC_ON_CHAPTER_OPEN, SYNC_ON_APP_START)
}
fun syncHost() = preferenceStore.getString("sync_host", "https://sync.tachiyomi.org") fun syncHost() = preferenceStore.getString("sync_host", "https://sync.tachiyomi.org")
fun syncAPIKey() = preferenceStore.getString("sync_api_key", "") fun syncAPIKey() = preferenceStore.getString("sync_api_key", "")
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L) fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
@ -22,4 +34,6 @@ class SyncPreferences(
Preference.appStateKey("google_drive_refresh_token"), Preference.appStateKey("google_drive_refresh_token"),
"", "",
) )
fun syncFlags() = preferenceStore.getInt("sync_flags", Flags.Defaults)
} }

View File

@ -566,7 +566,11 @@
<string name="google_drive_not_signed_in">Not signed in to Google Drive</string> <string name="google_drive_not_signed_in">Not signed in to Google Drive</string>
<string name="pref_purge_confirmation_title">Purge confirmation</string> <string name="pref_purge_confirmation_title">Purge confirmation</string>
<string name="pref_purge_confirmation_message">Purging sync data will delete all your sync data from Google Drive. Are you sure you want to continue?</string> <string name="pref_purge_confirmation_message">Purging sync data will delete all your sync data from Google Drive. Are you sure you want to continue?</string>
<string name="pref_sync_options">Create sync triggers</string>
<string name="pref_sync_options_summ">Can be used to set sync triggers</string>
<string name="sync_on_chapter_read">On chapter read</string>
<string name="sync_on_chapter_open">On every open chapter page (EXPERIMENTAL NOT RECOMMENDED, everytime you go to next page it or previous it will sync.)</string>
<string name="sync_on_app_start">On app start</string>
<!-- Advanced section --> <!-- Advanced section -->
<string name="label_network">Networking</string> <string name="label_network">Networking</string>