Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Port cross device sync #89

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions app/src/main/java/dev/yokai/data/sync/models/SyncTriggerOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dev.yokai.data.sync.models

import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import kotlinx.collections.immutable.persistentListOf

data class SyncTriggerOptions(
val syncOnChapterRead: Boolean = false,
val syncOnChapterOpen: Boolean = false,
val syncOnAppStart: Boolean = false,
val syncOnAppResume: Boolean = false,
val syncOnLibraryUpdate: Boolean = false,
) {
fun asBooleanArray() = booleanArrayOf(
syncOnChapterRead,
syncOnChapterOpen,
syncOnAppStart,
syncOnAppResume,
syncOnLibraryUpdate,
)

fun anyEnabled() = syncOnChapterRead ||
syncOnChapterOpen ||
syncOnAppStart ||
syncOnAppResume ||
syncOnLibraryUpdate

companion object {
val mainOptions = persistentListOf(
Entry(
label = R.string.sync_on_chapter_read,
getter = SyncTriggerOptions::syncOnChapterRead,
setter = { options, enabled -> options.copy(syncOnChapterRead = enabled) },
),
Entry(
label = R.string.sync_on_chapter_open,
getter = SyncTriggerOptions::syncOnChapterOpen,
setter = { options, enabled -> options.copy(syncOnChapterOpen = enabled) },
),
Entry(
label = R.string.sync_on_app_start,
getter = SyncTriggerOptions::syncOnAppStart,
setter = { options, enabled -> options.copy(syncOnAppStart = enabled) },
),
Entry(
label = R.string.sync_on_app_resume,
getter = SyncTriggerOptions::syncOnAppResume,
setter = { options, enabled -> options.copy(syncOnAppResume = enabled) },
),
Entry(
label = R.string.sync_on_library_update,
getter = SyncTriggerOptions::syncOnLibraryUpdate,
setter = { options, enabled -> options.copy(syncOnLibraryUpdate = enabled) },
),
)

fun fromBooleanArray(array: BooleanArray) = SyncTriggerOptions(
syncOnChapterRead = array[0],
syncOnChapterOpen = array[1],
syncOnAppStart = array[2],
syncOnAppResume = array[3],
syncOnLibraryUpdate = array[4],
)
}

data class Entry(
@StringRes val label: Int,
val getter: (SyncTriggerOptions) -> Boolean,
val setter: (SyncTriggerOptions, Boolean) -> SyncTriggerOptions,
val enabled: (SyncTriggerOptions) -> Boolean = { true },
)
}
92 changes: 92 additions & 0 deletions app/src/main/java/dev/yokai/domain/sync/SyncPreferences.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package dev.yokai.domain.sync

import dev.yokai.data.sync.models.SyncTriggerOptions
import dev.yokai.domain.sync.models.SyncSettings
import eu.kanade.tachiyomi.core.preference.Preference.Companion.appStateKey
import eu.kanade.tachiyomi.core.preference.PreferenceStore
import java.util.*

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(appStateKey("last_sync_timestamp"), 0L)

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

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

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

fun uniqueDeviceID(): String {
val uniqueIDPreference = preferenceStore.getString(appStateKey("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/dev/yokai/domain/sync/models/SyncSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.yokai.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 @@ -22,16 +22,19 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_HIDE_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_IS_SYNCING
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_UPDATE_STRATEGY
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VERSION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
import eu.kanade.tachiyomi.data.database.updateStrategyAdapter
import eu.kanade.tachiyomi.util.system.toBoolean

class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(),
Expand Down Expand Up @@ -71,6 +74,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
put(COL_DATE_ADDED, obj.date_added)
put(COL_FILTERED_SCANLATORS, obj.filtered_scanlators)
put(COL_UPDATE_STRATEGY, obj.update_strategy.let(updateStrategyAdapter::encode))
put(COL_VERSION, obj.version)
put(COL_IS_SYNCING, obj.isSyncing)
}
}

Expand All @@ -86,17 +91,19 @@ interface BaseMangaGetResolver {
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)).toBoolean()
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)).toBoolean()
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)) == 1
hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)).toBoolean()
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
update_strategy = cursor.getInt(cursor.getColumnIndex(COL_UPDATE_STRATEGY)).let(
updateStrategyAdapter::decode,
)
version = cursor.getInt(cursor.getColumnIndex(COL_VERSION))
isSyncing = cursor.getInt(cursor.getColumnIndex(COL_IS_SYNCING)).toBoolean()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import eu.kanade.tachiyomi.util.system.toBoolean
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import java.util.*

interface Manga : SManga {

Expand All @@ -34,6 +35,10 @@ interface Manga : SManga {

var filtered_scanlators: String?

var version: Int

var isSyncing: Boolean

fun isBlank() = id == Long.MIN_VALUE
fun isHidden() = status == -1

Expand Down Expand Up @@ -359,7 +364,9 @@ interface Manga : SManga {
chapterFlags: Long,
dateAdded: Long?,
filteredScanlators: String?,
updateStrategy: Long
updateStrategy: Long,
version: Long,
isSyncing: Long,
): Manga = create(source).apply {
this.id = id
this.url = url
Expand All @@ -370,15 +377,17 @@ interface Manga : SManga {
this.title = title
this.status = status.toInt()
this.thumbnail_url = thumbnailUrl
this.favorite = favorite > 0
this.favorite = favorite.toBoolean()
this.last_update = lastUpdate ?: 0L
this.initialized = initialized
this.viewer_flags = viewerFlags.toInt()
this.chapter_flags = chapterFlags.toInt()
this.hide_title = hideTitle > 0
this.hide_title = hideTitle.toBoolean()
this.date_added = dateAdded ?: 0L
this.filtered_scanlators = filteredScanlators
this.update_strategy = updateStrategy.toInt().let(updateStrategyAdapter::decode)
this.version = version.toInt()
this.isSyncing = isSyncing.toBoolean()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ object MangaTable {

const val COL_UPDATE_STRATEGY = "update_strategy"

const val COL_VERSION = "version"

const val COL_IS_SYNCING = "is_syncing"
}
18 changes: 17 additions & 1 deletion app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("FunctionName", "unused")

package eu.kanade.tachiyomi.network

import okhttp3.CacheControl
Expand All @@ -7,7 +9,7 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import java.util.concurrent.TimeUnit.MINUTES
import java.util.concurrent.TimeUnit.*

private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build()
Expand Down Expand Up @@ -64,6 +66,20 @@ fun PUT(
.build()
}

fun PATCH(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
.patch(body)
.headers(headers)
.cacheControl(cache)
.build()
}

fun DELETE(
url: String,
headers: Headers = DEFAULT_HEADERS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.util.system

fun Boolean.toInt() = if (this) 1 else 0
fun Int.toBoolean() = this == 1
fun Number.toBoolean() = this == 1
Loading
Loading