Skip to content

Commit

Permalink
feat: add cross device sync (#1005)
Browse files Browse the repository at this point in the history
* feat: add cross device sync.

* chore: add google api.

* chore: add SY specifics.

* feat: add backupSource, backupPref, and "SY" backupSavedSearches.

I forgot to add the data into the merging logic, So remote always have empty value :(. Better late than never.

* feat(sync): Allow to choose what to sync.

Various improvement and added the option to choose what they want to sync. Added sync library button to LibraryTab as well.

Signed-off-by: KaiserBh <[email protected]>

* oops.

Signed-off-by: KaiserBh <[email protected]>

* refactor: fix up the sync triggers, and update imports.

* refactor

* chore: review pointers.

* refactor: update imports

* refactor: add more error guard for gdrive.

Also changed it to be app specific, we don't want them to use sync data from SY or other forks as some of the model and backup is different. So if people using other forks they should use the same data and not mismatch.

* fix: conflict and refactor.

* refactor: update imports.

* chore: fix some of detekt error.

* refactor: add breaks and max retries.

I think we were reaching deadlock or infinite loop causing the sync to go forever.

* feat: db changes to accommodate new syncing logic.

Using timestamp to sync is a bit skewed due to system clock etc and therefore there was a lot of issues with it such as removing a manga that shouldn't have been removed. Marking chapters as unread even though it was marked as a read. Hopefully by using versioning system it should eliminate those issues.

* chore: add migrations

* chore: version and is_syncing fields.

* chore: add SY only stuff.

* fix: oops wrong index.

Signed-off-by: KaiserBh <[email protected]>

* chore: review pointers.

Signed-off-by: KaiserBh <[email protected]>

* chore: remove the option to reset timestamp

We don't need this anymore since we are utilizing versioning system.

Signed-off-by: KaiserBh <[email protected]>

* refactor: Forgot to use the new versioning system.

I forgot to cherry pick this from mihon branch.

* chore: remove isSyncing from Chapter/Manga model.

Signed-off-by: KaiserBh <[email protected]>

* chore: remove unused import.

Signed-off-by: KaiserBh <[email protected]>

* chore: remove isSyncing leftover.

Signed-off-by: KaiserBh <[email protected]>

* chore: remove isSyncing.

Signed-off-by: KaiserBh <[email protected]>

* refactor: make sure the manga version is bumped.

When there is changes in the chapters table such as reading or updating last read page we should bump the manga version. This way the manga is synced properly as in the History and last_read history is synced properly. This should fix the sorting issue.

Signed-off-by: KaiserBh <[email protected]>

---------

Signed-off-by: KaiserBh <[email protected]>
  • Loading branch information
kaiserbh authored and cuong-tran committed Mar 18, 2024
1 parent 5299802 commit edcd339
Show file tree
Hide file tree
Showing 37 changed files with 2,771 additions and 46 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@ dependencies {
implementation(libs.compose.materialmotion)
implementation(libs.swipe)

implementation(libs.google.api.services.drive)
implementation(libs.google.api.client.oauth)

// Logging
implementation(libs.logcat)

Expand Down
23 changes: 22 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@
-keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; }

# Google Drive
-keep class com.google.api.services.** { *; }

# Google OAuth
-keep class com.google.api.client.** { *; }

# SY -->
# SqlCipher
-keepclassmembers class net.zetetic.database.sqlcipher.SQLiteCustomFunction { *; }
Expand Down Expand Up @@ -260,6 +266,9 @@
-keep,allowoptimization class * extends uy.kohesive.injekt.api.TypeReference
-keep,allowoptimization public class io.requery.android.database.sqlite.SQLiteConnection { *; }

# Keep apache http client
-keep class org.apache.http.** { *; }

# Suggested rules
-dontwarn com.oracle.svm.core.annotate.AutomaticFeature
-dontwarn com.oracle.svm.core.annotate.Delete
Expand All @@ -272,4 +281,16 @@
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn java.lang.Module
-dontwarn org.graalvm.nativeimage.hosted.RuntimeResourceAccess
-dontwarn org.jspecify.annotations.NullMarked
-dontwarn org.jspecify.annotations.NullMarked
-dontwarn javax.naming.InvalidNameException
-dontwarn javax.naming.NamingException
-dontwarn javax.naming.directory.Attribute
-dontwarn javax.naming.directory.Attributes
-dontwarn javax.naming.ldap.LdapName
-dontwarn javax.naming.ldap.Rdn
-dontwarn org.ietf.jgss.GSSContext
-dontwarn org.ietf.jgss.GSSCredential
-dontwarn org.ietf.jgss.GSSException
-dontwarn org.ietf.jgss.GSSManager
-dontwarn org.ietf.jgss.GSSName
-dontwarn org.ietf.jgss.Oid
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@
<data android:host="shikimori-auth" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.GoogleDriveLoginActivity"
android:label="GoogleDrive"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="eu.kanade.google.oauth" />
</intent-filter>
</activity>

<activity
android:name="exh.ui.login.EhLoginActivity"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/assets/client_secrets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"installed":{"client_id":"1046609911130-tbp79niehhuii976ekep1us06e9a8lne.apps.googleusercontent.com","project_id":"tachiyomi","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
it.date_upload = dateUpload
it.chapter_number = chapterNumber.toFloat()
it.source_order = sourceOrder.toInt()
it.last_modified = lastModifiedAt
}
92 changes: 92 additions & 0 deletions app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package eu.kanade.domain.sync

import eu.kanade.domain.sync.models.SyncSettings
import eu.kanade.tachiyomi.data.sync.models.SyncTriggerOptions
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import java.util.UUID

class SyncPreferences(
private val preferenceStore: PreferenceStore,
) {
fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org")
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)

fun syncInterval() = preferenceStore.getInt("sync_interval", 0)
fun syncService() = preferenceStore.getInt("sync_service", 0)

fun googleDriveAccessToken() = preferenceStore.getString(
Preference.appStateKey("google_drive_access_token"),
"",
)

fun googleDriveRefreshToken() = preferenceStore.getString(
Preference.appStateKey("google_drive_refresh_token"),
"",
)

fun uniqueDeviceID(): String {
val uniqueIDPreference = preferenceStore.getString("unique_device_id", "")

// Retrieve the current value of the preference
var uniqueID = uniqueIDPreference.get()
if (uniqueID.isBlank()) {
uniqueID = UUID.randomUUID().toString()
uniqueIDPreference.set(uniqueID)
}

return uniqueID
}

fun isSyncEnabled(): Boolean {
return syncService().get() != 0
}

fun getSyncSettings(): SyncSettings {
return SyncSettings(
libraryEntries = preferenceStore.getBoolean("library_entries", true).get(),
categories = preferenceStore.getBoolean("categories", true).get(),
chapters = preferenceStore.getBoolean("chapters", true).get(),
tracking = preferenceStore.getBoolean("tracking", true).get(),
history = preferenceStore.getBoolean("history", true).get(),
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
)
}

fun setSyncSettings(syncSettings: SyncSettings) {
preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries)
preferenceStore.getBoolean("categories", true).set(syncSettings.categories)
preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters)
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
preferenceStore.getBoolean("history", true).set(syncSettings.history)
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
}

fun getSyncTriggerOptions(): SyncTriggerOptions {
return SyncTriggerOptions(
syncOnChapterRead = preferenceStore.getBoolean("sync_on_chapter_read", false).get(),
syncOnChapterOpen = preferenceStore.getBoolean("sync_on_chapter_open", false).get(),
syncOnAppStart = preferenceStore.getBoolean("sync_on_app_start", false).get(),
syncOnAppResume = preferenceStore.getBoolean("sync_on_app_resume", false).get(),
syncOnLibraryUpdate = preferenceStore.getBoolean("sync_on_library_update", false).get(),
)
}

fun setSyncTriggerOptions(syncTriggerOptions: SyncTriggerOptions) {
preferenceStore.getBoolean("sync_on_chapter_read", false)
.set(syncTriggerOptions.syncOnChapterRead)
preferenceStore.getBoolean("sync_on_chapter_open", false)
.set(syncTriggerOptions.syncOnChapterOpen)
preferenceStore.getBoolean("sync_on_app_start", false)
.set(syncTriggerOptions.syncOnAppStart)
preferenceStore.getBoolean("sync_on_app_resume", false)
.set(syncTriggerOptions.syncOnAppResume)
preferenceStore.getBoolean("sync_on_library_update", false)
.set(syncTriggerOptions.syncOnLibraryUpdate)
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package eu.kanade.domain.sync.models

data class SyncSettings(
val libraryEntries: Boolean = true,
val categories: Boolean = true,
val chapters: Boolean = true,
val tracking: Boolean = true,
val history: Boolean = true,
val appSettings: Boolean = true,
val sourceSettings: Boolean = true,
val privateSettings: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fun LibraryToolbar(
onClickRefresh: () -> Unit,
onClickGlobalUpdate: () -> Unit,
onClickOpenRandomManga: () -> Unit,
onClickSyncNow: () -> Unit,
// SY -->
onClickSyncExh: (() -> Unit)?,
// SY <--
Expand All @@ -60,6 +61,7 @@ fun LibraryToolbar(
onClickRefresh = onClickRefresh,
onClickGlobalUpdate = onClickGlobalUpdate,
onClickOpenRandomManga = onClickOpenRandomManga,
onClickSyncNow = onClickSyncNow,
// SY -->
onClickSyncExh = onClickSyncExh,
// SY <--
Expand All @@ -77,6 +79,7 @@ private fun LibraryRegularToolbar(
onClickRefresh: () -> Unit,
onClickGlobalUpdate: () -> Unit,
onClickOpenRandomManga: () -> Unit,
onClickSyncNow: () -> Unit,
// SY -->
onClickSyncExh: (() -> Unit)?,
// SY <--
Expand Down Expand Up @@ -125,7 +128,10 @@ private fun LibraryRegularToolbar(
title = stringResource(MR.strings.action_open_random_manga),
onClick = onClickOpenRandomManga,
),

AppBar.OverflowAction(
title = stringResource(MR.strings.sync_library),
onClick = onClickSyncNow,
),
).builder().apply {
// SY -->
if (onClickSyncExh != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
Expand Down
Loading

0 comments on commit edcd339

Please sign in to comment.