mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 17:29:14 +01:00
feat: added google drive service.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
parent
536c5facb9
commit
645505e1e9
@ -257,6 +257,9 @@ dependencies {
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
|
||||
implementation(libs.google.api.services.drive)
|
||||
implementation(libs.google.api.client.oauth)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
|
6
app/proguard-rules.pro
vendored
6
app/proguard-rules.pro
vendored
@ -74,3 +74,9 @@
|
||||
# Firebase
|
||||
-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.** { *; }
|
@ -183,6 +183,20 @@
|
||||
android:scheme="tachiyomi" />
|
||||
</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>
|
||||
|
||||
<receiver
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
|
1
app/src/main/assets/client_secrets.json
Normal file
1
app/src/main/assets/client_secrets.json
Normal file
@ -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"}}
|
@ -52,6 +52,8 @@ import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
@ -94,6 +96,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
|
||||
entries = mapOf(
|
||||
SyncManager.SyncService.NONE.value to stringResource(R.string.off),
|
||||
SyncManager.SyncService.SYNCYOMI.value to stringResource(R.string.syncyomi),
|
||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(R.string.google_drive),
|
||||
),
|
||||
onValueChanged = { true },
|
||||
),
|
||||
@ -448,6 +451,7 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
|
||||
return when (syncServiceType) {
|
||||
SyncManager.SyncService.NONE -> emptyList()
|
||||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||
} +
|
||||
if (syncServiceType == SyncManager.SyncService.NONE) {
|
||||
emptyList()
|
||||
@ -456,6 +460,74 @@ object SettingsBackupAndSyncScreen : SearchableSettings {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGoogleDrivePreferences(): List<Preference> {
|
||||
val context = LocalContext.current
|
||||
val googleDriveSync = Injekt.get<GoogleDriveService>()
|
||||
return listOf(
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_google_drive_sign_in),
|
||||
onClick = {
|
||||
val intent = googleDriveSync.getSignInIntent()
|
||||
context.startActivity(intent)
|
||||
},
|
||||
),
|
||||
getGoogleDrivePurge(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGoogleDrivePurge(): Preference.PreferenceItem.TextPreference {
|
||||
val scope = rememberCoroutineScope()
|
||||
val showPurgeDialog = remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
val googleDriveSync = remember { GoogleDriveSyncService(context) }
|
||||
|
||||
if (showPurgeDialog.value) {
|
||||
PurgeConfirmationDialog(
|
||||
onConfirm = {
|
||||
showPurgeDialog.value = false
|
||||
scope.launch {
|
||||
val result = googleDriveSync.deleteSyncDataFromGoogleDrive()
|
||||
if (result) {
|
||||
context.toast(R.string.google_drive_sync_data_purged)
|
||||
} else {
|
||||
context.toast(R.string.google_drive_sync_data_not_found)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissRequest = { showPurgeDialog.value = false },
|
||||
)
|
||||
}
|
||||
|
||||
return Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(R.string.pref_google_drive_purge_sync_data),
|
||||
onClick = { showPurgeDialog.value = true },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PurgeConfirmationDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(R.string.pref_purge_confirmation_title)) },
|
||||
text = { Text(text = stringResource(R.string.pref_purge_confirmation_message)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSelfHostPreferences(syncPreferences: SyncPreferences): List<Preference> {
|
||||
return listOf(
|
||||
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.network.JavaScriptEngine
|
||||
@ -151,6 +152,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
get<DownloadManager>()
|
||||
}
|
||||
|
||||
addSingletonFactory { GoogleDriveService(app) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.sync.models.SyncData
|
||||
import eu.kanade.tachiyomi.data.sync.models.SyncDevice
|
||||
import eu.kanade.tachiyomi.data.sync.models.SyncStatus
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
|
||||
import eu.kanade.tachiyomi.data.sync.service.SyncYomiSyncService
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
@ -56,6 +57,7 @@ class SyncManager(
|
||||
enum class SyncService(val value: Int) {
|
||||
NONE(0),
|
||||
SYNCYOMI(1),
|
||||
GOOGLE_DRIVE(2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
@ -107,6 +109,10 @@ class SyncManager(
|
||||
)
|
||||
}
|
||||
|
||||
SyncService.GOOGLE_DRIVE -> {
|
||||
GoogleDriveSyncService(context, json, syncPreferences)
|
||||
}
|
||||
|
||||
else -> {
|
||||
logcat(LogPriority.ERROR) { "Invalid sync service type: $syncService" }
|
||||
null
|
||||
|
@ -0,0 +1,332 @@
|
||||
package eu.kanade.tachiyomi.data.sync.service
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.google.api.client.auth.oauth2.TokenResponseException
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse
|
||||
import com.google.api.client.http.ByteArrayContent
|
||||
import com.google.api.client.http.javanet.NetHttpTransport
|
||||
import com.google.api.client.json.JsonFactory
|
||||
import com.google.api.client.json.jackson2.JacksonFactory
|
||||
import com.google.api.services.drive.Drive
|
||||
import com.google.api.services.drive.DriveScopes
|
||||
import com.google.api.services.drive.model.File
|
||||
import eu.kanade.tachiyomi.data.sync.models.SyncData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.sync.SyncPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: SyncPreferences) : SyncService(context, json, syncPreferences) {
|
||||
constructor(context: Context) : this(
|
||||
context,
|
||||
Json {
|
||||
encodeDefaults = true
|
||||
ignoreUnknownKeys = true
|
||||
},
|
||||
Injekt.get<SyncPreferences>(),
|
||||
)
|
||||
|
||||
private val remoteFileName = "tachiyomi_sync_data.gz"
|
||||
|
||||
private val googleDriveService = GoogleDriveService(context)
|
||||
|
||||
override suspend fun beforeSync() = googleDriveService.refreshToken()
|
||||
|
||||
override suspend fun pushSyncData(): SyncData? {
|
||||
val drive = googleDriveService.googleDriveService
|
||||
|
||||
// Check if the Google Drive service is initialized
|
||||
if (drive == null) {
|
||||
logcat(LogPriority.ERROR) { "Google Drive service not initialized" }
|
||||
return null
|
||||
}
|
||||
|
||||
val fileList = getFileList(drive)
|
||||
|
||||
if (fileList.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val gdriveFileId = fileList[0].id
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
drive.files().get(gdriveFileId).executeMediaAndDownloadTo(outputStream)
|
||||
val jsonString = withContext(Dispatchers.IO) {
|
||||
val gzipInputStream = GZIPInputStream(ByteArrayInputStream(outputStream.toByteArray()))
|
||||
gzipInputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
||||
}
|
||||
|
||||
return json.decodeFromString(SyncData.serializer(), jsonString)
|
||||
}
|
||||
|
||||
override suspend fun pullSyncData(syncData: SyncData) {
|
||||
val jsonData = json.encodeToString(syncData)
|
||||
|
||||
val drive = googleDriveService.googleDriveService
|
||||
|
||||
// Check if the Google Drive service is initialized
|
||||
if (drive == null) {
|
||||
logcat(LogPriority.ERROR) { "Google Drive service not initialized" }
|
||||
return
|
||||
}
|
||||
|
||||
// delete file if exists
|
||||
val fileList = getFileList(drive)
|
||||
if (fileList.isNotEmpty()) {
|
||||
drive.files().delete(fileList[0].id).execute()
|
||||
}
|
||||
|
||||
val fileMetadata = File()
|
||||
fileMetadata.name = remoteFileName
|
||||
fileMetadata.mimeType = "application/gzip"
|
||||
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val gzipOutputStream = GZIPOutputStream(byteArrayOutputStream)
|
||||
gzipOutputStream.write(jsonData.toByteArray(Charsets.UTF_8))
|
||||
gzipOutputStream.close()
|
||||
}
|
||||
|
||||
val byteArrayContent = ByteArrayContent("application/octet-stream", byteArrayOutputStream.toByteArray())
|
||||
val uploadedFile = drive.files().create(fileMetadata, byteArrayContent)
|
||||
.setFields("id")
|
||||
.execute()
|
||||
|
||||
logcat(LogPriority.DEBUG) { "Created sync data file in Google Drive with file ID: ${uploadedFile.id}" }
|
||||
}
|
||||
|
||||
private fun getFileList(drive: Drive): MutableList<File> {
|
||||
// Search for the existing file by name
|
||||
val query = "mimeType='application/gzip' and trashed = false and name = '$remoteFileName'"
|
||||
val fileList = drive.files().list().setQ(query).execute().files
|
||||
Log.d("GoogleDrive", "File list: $fileList")
|
||||
|
||||
return fileList
|
||||
}
|
||||
|
||||
suspend fun deleteSyncDataFromGoogleDrive(): Boolean {
|
||||
val drive = googleDriveService.googleDriveService
|
||||
|
||||
if (drive == null) {
|
||||
logcat(LogPriority.ERROR) { "Google Drive service not initialized" }
|
||||
return false
|
||||
}
|
||||
googleDriveService.refreshToken()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val query = "mimeType='application/gzip' and trashed = false and name = '$remoteFileName'"
|
||||
val fileList = drive.files().list().setQ(query).execute().files
|
||||
|
||||
if (fileList.isNullOrEmpty()) {
|
||||
logcat(LogPriority.DEBUG) { "No sync data file found in Google Drive" }
|
||||
false
|
||||
} else {
|
||||
val fileId = fileList[0].id
|
||||
drive.files().delete(fileId).execute()
|
||||
logcat(LogPriority.DEBUG) { "Deleted sync data file in Google Drive with file ID: $fileId" }
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GoogleDriveService(private val context: Context) {
|
||||
var googleDriveService: Drive? = null
|
||||
private val syncPreferences = Injekt.get<SyncPreferences>()
|
||||
|
||||
init {
|
||||
initGoogleDriveService()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Google Drive service by obtaining the access token and refresh token from the SyncPreferences
|
||||
* and setting up the service using the obtained tokens.
|
||||
*/
|
||||
private fun initGoogleDriveService() {
|
||||
val accessToken = syncPreferences.getGoogleDriveAccessToken()
|
||||
val refreshToken = syncPreferences.getGoogleDriveRefreshToken()
|
||||
|
||||
if (accessToken == "" || refreshToken == "") {
|
||||
googleDriveService = null
|
||||
return
|
||||
}
|
||||
|
||||
setupGoogleDriveService(accessToken, refreshToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches an Intent to open the user's default browser for Google Drive sign-in.
|
||||
* The Intent carries the authorization URL, which prompts the user to sign in
|
||||
* and grant the application permission to access their Google Drive account.
|
||||
* @return An Intent configured to launch a browser for Google Drive OAuth sign-in.
|
||||
*/
|
||||
fun getSignInIntent(): Intent {
|
||||
val authorizationUrl = generateAuthorizationUrl()
|
||||
|
||||
return Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(authorizationUrl)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the authorization URL required for the user to grant the application permission to access their Google Drive account.
|
||||
* Sets the approval prompt to "force" to ensure that the user is always prompted to grant access, even if they have previously granted access.
|
||||
* @return The authorization URL.
|
||||
*/
|
||||
private fun generateAuthorizationUrl(): String {
|
||||
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
|
||||
val secrets = GoogleClientSecrets.load(
|
||||
jsonFactory,
|
||||
InputStreamReader(context.assets.open("client_secrets.json")),
|
||||
)
|
||||
|
||||
val flow = GoogleAuthorizationCodeFlow.Builder(
|
||||
NetHttpTransport(),
|
||||
jsonFactory,
|
||||
secrets,
|
||||
listOf(DriveScopes.DRIVE_FILE),
|
||||
).setAccessType("offline").build()
|
||||
|
||||
return flow.newAuthorizationUrl()
|
||||
.setRedirectUri("eu.kanade.google.oauth:/oauth2redirect")
|
||||
.setApprovalPrompt("force")
|
||||
.build()
|
||||
}
|
||||
internal suspend fun refreshToken() = withContext(Dispatchers.IO) {
|
||||
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
|
||||
val secrets = GoogleClientSecrets.load(
|
||||
jsonFactory,
|
||||
InputStreamReader(context.assets.open("client_secrets.json")),
|
||||
)
|
||||
|
||||
val credential = GoogleCredential.Builder()
|
||||
.setJsonFactory(jsonFactory)
|
||||
.setTransport(NetHttpTransport())
|
||||
.setClientSecrets(secrets)
|
||||
.build()
|
||||
|
||||
credential.refreshToken = syncPreferences.getGoogleDriveRefreshToken()
|
||||
|
||||
logcat(LogPriority.DEBUG) { "Refreshing access token with: ${syncPreferences.getGoogleDriveRefreshToken()}" }
|
||||
|
||||
try {
|
||||
credential.refreshToken()
|
||||
val newAccessToken = credential.accessToken
|
||||
val oldAccessToken = syncPreferences.getGoogleDriveAccessToken()
|
||||
// Save the new access token
|
||||
syncPreferences.setGoogleDriveAccessToken(newAccessToken)
|
||||
setupGoogleDriveService(newAccessToken, credential.refreshToken)
|
||||
logcat(LogPriority.DEBUG) { "Google Access token refreshed old: $oldAccessToken new: $newAccessToken" }
|
||||
} catch (e: TokenResponseException) {
|
||||
if (e.details.error == "invalid_grant") {
|
||||
// The refresh token is invalid, prompt the user to sign in again
|
||||
logcat(LogPriority.ERROR) { "Refresh token is invalid, prompt user to sign in again" }
|
||||
throw e.message?.let { Exception(it) } ?: Exception("Unknown error")
|
||||
} else {
|
||||
// Token refresh failed; handle this situation
|
||||
logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" }
|
||||
logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// Token refresh failed; handle this situation
|
||||
logcat(LogPriority.ERROR) { "Failed to refresh access token ${e.message}" }
|
||||
logcat(LogPriority.ERROR) { "Google Drive sync will be disabled" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the Google Drive service using the provided access token and refresh token.
|
||||
* @param accessToken The access token obtained from the SyncPreferences.
|
||||
* @param refreshToken The refresh token obtained from the SyncPreferences.
|
||||
*/
|
||||
private fun setupGoogleDriveService(accessToken: String, refreshToken: String) {
|
||||
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
|
||||
val secrets = GoogleClientSecrets.load(
|
||||
jsonFactory,
|
||||
InputStreamReader(context.assets.open("client_secrets.json")),
|
||||
)
|
||||
|
||||
val credential = GoogleCredential.Builder()
|
||||
.setJsonFactory(jsonFactory)
|
||||
.setTransport(NetHttpTransport())
|
||||
.setClientSecrets(secrets)
|
||||
.build()
|
||||
|
||||
credential.accessToken = accessToken
|
||||
credential.refreshToken = refreshToken
|
||||
|
||||
googleDriveService = Drive.Builder(
|
||||
NetHttpTransport(),
|
||||
jsonFactory,
|
||||
credential,
|
||||
).setApplicationName("Tachiyomi")
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authorization code returned after the user has granted the application permission to access their Google Drive account.
|
||||
* It obtains the access token and refresh token using the authorization code, saves the tokens to the SyncPreferences,
|
||||
* sets up the Google Drive service using the obtained tokens, and initializes the service.
|
||||
* @param authorizationCode The authorization code obtained from the OAuthCallbackServer.
|
||||
* @param activity The current activity.
|
||||
* @param onSuccess A callback function to be called on successful authorization.
|
||||
* @param onFailure A callback function to be called on authorization failure.
|
||||
*/
|
||||
fun handleAuthorizationCode(authorizationCode: String, activity: Activity, onSuccess: () -> Unit, onFailure: (String) -> Unit) {
|
||||
val jsonFactory: JsonFactory = JacksonFactory.getDefaultInstance()
|
||||
val secrets = GoogleClientSecrets.load(
|
||||
jsonFactory,
|
||||
InputStreamReader(context.assets.open("client_secrets.json")),
|
||||
)
|
||||
|
||||
val tokenResponse: GoogleTokenResponse = GoogleAuthorizationCodeTokenRequest(
|
||||
NetHttpTransport(),
|
||||
jsonFactory,
|
||||
secrets.installed.clientId,
|
||||
secrets.installed.clientSecret,
|
||||
authorizationCode,
|
||||
"eu.kanade.google.oauth:/oauth2redirect",
|
||||
).setGrantType("authorization_code").execute()
|
||||
|
||||
try {
|
||||
// Save the access token and refresh token
|
||||
val accessToken = tokenResponse.accessToken
|
||||
val refreshToken = tokenResponse.refreshToken
|
||||
|
||||
// Save the tokens to SyncPreferences
|
||||
syncPreferences.setGoogleDriveAccessToken(accessToken)
|
||||
syncPreferences.setGoogleDriveRefreshToken(refreshToken)
|
||||
|
||||
setupGoogleDriveService(accessToken, refreshToken)
|
||||
initGoogleDriveService()
|
||||
|
||||
activity.runOnUiThread {
|
||||
onSuccess()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
activity.runOnUiThread {
|
||||
onFailure(e.localizedMessage ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -106,8 +106,8 @@ abstract class SyncService(
|
||||
localMangaMap.forEach { (key, localManga) ->
|
||||
val remoteManga = remoteMangaMap[key]
|
||||
if (remoteManga != null) {
|
||||
val localInstant = localManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
|
||||
val remoteInstant = remoteManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
|
||||
val localInstant = localManga.lastModifiedAt.let { Instant.ofEpochMilli(it) }
|
||||
val remoteInstant = remoteManga.lastModifiedAt.let { Instant.ofEpochMilli(it) }
|
||||
|
||||
val mergedManga = if ((localInstant ?: Instant.MIN) >= (
|
||||
remoteInstant
|
||||
@ -164,8 +164,8 @@ abstract class SyncService(
|
||||
localChapterMap.forEach { (url, localChapter) ->
|
||||
val remoteChapter = remoteChapterMap[url]
|
||||
if (remoteChapter != null) {
|
||||
val localInstant = localChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
|
||||
val remoteInstant = remoteChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
|
||||
val localInstant = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) }
|
||||
val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) }
|
||||
|
||||
val mergedChapter =
|
||||
if ((localInstant ?: Instant.MIN) >= (remoteInstant ?: Instant.MIN)) {
|
||||
|
@ -0,0 +1,52 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class GoogleDriveLoginActivity : BaseOAuthLoginActivity() {
|
||||
private val googleDriveService = Injekt.get<GoogleDriveService>()
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
val error = data?.getQueryParameter("error")
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
googleDriveService.handleAuthorizationCode(
|
||||
code,
|
||||
this@GoogleDriveLoginActivity,
|
||||
onSuccess = {
|
||||
Toast.makeText(
|
||||
this@GoogleDriveLoginActivity,
|
||||
"Authorization successful.",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
|
||||
returnToSettings()
|
||||
},
|
||||
onFailure = { error ->
|
||||
Toast.makeText(
|
||||
this@GoogleDriveLoginActivity,
|
||||
"Authorization failed: $error",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
returnToSettings()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if (error != null) {
|
||||
Toast.makeText(
|
||||
this@GoogleDriveLoginActivity,
|
||||
"Authorization failed: $error",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
|
||||
returnToSettings()
|
||||
} else {
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
}
|
@ -15,4 +15,20 @@ class SyncPreferences(
|
||||
fun deviceName() = preferenceStore.getString("device_name", android.os.Build.MANUFACTURER + android.os.Build.PRODUCT)
|
||||
|
||||
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
||||
|
||||
private fun googleDriveAccessToken() = preferenceStore.getString("google_drive_access_token", "")
|
||||
|
||||
fun setGoogleDriveAccessToken(accessToken: String) {
|
||||
googleDriveAccessToken().set(accessToken)
|
||||
}
|
||||
|
||||
fun getGoogleDriveAccessToken() = googleDriveAccessToken().get()
|
||||
|
||||
private fun googleDriveRefreshToken() = preferenceStore.getString("google_drive_refresh_token", "")
|
||||
|
||||
fun setGoogleDriveRefreshToken(refreshToken: String) {
|
||||
googleDriveRefreshToken().set(refreshToken)
|
||||
}
|
||||
|
||||
fun getGoogleDriveRefreshToken() = googleDriveRefreshToken().get()
|
||||
}
|
||||
|
@ -91,6 +91,9 @@ voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.
|
||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
|
||||
google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197-1.25.0"
|
||||
google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
|
||||
|
||||
kotlinter = "org.jmailen.gradle:kotlinter-gradle:3.13.0"
|
||||
|
||||
[bundles]
|
||||
|
@ -530,6 +530,18 @@
|
||||
<string name="syncyomi">SyncYomi</string>
|
||||
<string name="sync_completed_message">Done in %1$s</string>
|
||||
<string name="last_synchronization">Last Synchronization: %1$s</string>
|
||||
<string name="google_drive">Google Drive</string>
|
||||
<string name="pref_google_drive_sign_in">Sign in</string>
|
||||
<string name="google_drive_sign_in_success">Signed in successfully</string>
|
||||
<string name="google_drive_sign_in_failed">Sign in failed</string>
|
||||
<string name="authentication">Authentication</string>
|
||||
<string name="pref_google_drive_purge_sync_data">Clear Sync Data from Google Drive</string>
|
||||
<string name="google_drive_sync_data_purged">Sync data purged from Google Drive</string>
|
||||
<string name="google_drive_sync_data_not_found">No sync data found in 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_message">Purging sync data will delete all your sync data from Google Drive. Are you sure you want to continue?</string>
|
||||
|
||||
|
||||
<!-- Advanced section -->
|
||||
<string name="label_network">Network</string>
|
||||
|
Loading…
Reference in New Issue
Block a user