fix(library): notify when sync fails before library update
Report sync failures that previously failed silently (e.g. null remote backup) and show a library update error notification when a chained sync blocks the update, including the sync failure reason. Co-authored-by: Cuong-Tran <cuong-tran@users.noreply.github.com>
This commit is contained in:
parent
30c114c20f
commit
9ccf5ca291
10 changed files with 143 additions and 24 deletions
|
|
@ -182,6 +182,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
|
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
try {
|
try {
|
||||||
|
LibraryUpdateSyncResult.consumeFailure()?.let { syncError ->
|
||||||
|
notifier.showUpdateErrorNotification(
|
||||||
|
failed = 1,
|
||||||
|
errorMessage = context.stringResource(
|
||||||
|
MR.strings.library_update_skipped_sync_error,
|
||||||
|
syncError,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return@withIOContext Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList()
|
Target.CHAPTERS -> updateChapterList()
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -844,6 +855,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
// Define the SyncDataJob
|
// Define the SyncDataJob
|
||||||
val syncDataJob = OneTimeWorkRequestBuilder<SyncDataJob>()
|
val syncDataJob = OneTimeWorkRequestBuilder<SyncDataJob>()
|
||||||
.addTag(SyncDataJob.TAG_MANUAL)
|
.addTag(SyncDataJob.TAG_MANUAL)
|
||||||
|
.addTag(SyncDataJob.TAG_BEFORE_LIBRARY_UPDATE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Chain SyncDataJob to run before LibraryUpdateJob
|
// Chain SyncDataJob to run before LibraryUpdateJob
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ class LibraryUpdateNotifier(
|
||||||
*
|
*
|
||||||
* @param failed Number of entries that failed to update.
|
* @param failed Number of entries that failed to update.
|
||||||
*/
|
*/
|
||||||
fun showUpdateErrorNotification(failed: Int) {
|
fun showUpdateErrorNotification(failed: Int, errorMessage: String? = null) {
|
||||||
if (failed == 0) {
|
if (failed == 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +163,7 @@ class LibraryUpdateNotifier(
|
||||||
Notifications.CHANNEL_LIBRARY_ERROR,
|
Notifications.CHANNEL_LIBRARY_ERROR,
|
||||||
) {
|
) {
|
||||||
setContentTitle(context.stringResource(MR.strings.notification_update_error, failed))
|
setContentTitle(context.stringResource(MR.strings.notification_update_error, failed))
|
||||||
setContentText(context.stringResource(MR.strings.action_show_errors))
|
setContentText(errorMessage ?: context.stringResource(MR.strings.action_show_errors))
|
||||||
setSmallIcon(R.drawable.ic_komikku)
|
setSmallIcon(R.drawable.ic_komikku)
|
||||||
setColor(ContextCompat.getColor(context, R.color.ic_launcher))
|
setColor(ContextCompat.getColor(context, R.color.ic_launcher))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a sync failure message to be reported by [LibraryUpdateJob] when a library update
|
||||||
|
* was chained to run after [eu.kanade.tachiyomi.data.sync.SyncDataJob].
|
||||||
|
*/
|
||||||
|
object LibraryUpdateSyncResult {
|
||||||
|
@Volatile
|
||||||
|
private var pendingError: String? = null
|
||||||
|
|
||||||
|
fun setFailure(message: String) {
|
||||||
|
pendingError = message
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeFailure(): String? {
|
||||||
|
val error = pendingError
|
||||||
|
pendingError = null
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,14 +14,18 @@ import androidx.work.WorkQuery
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.SyncStatus
|
import eu.kanade.tachiyomi.data.SyncStatus
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateSyncResult
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
@ -53,12 +57,22 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
SyncManager(context).syncData()
|
when (val result = SyncManager(context).syncData()) {
|
||||||
Result.success()
|
SyncDataResult.Success -> Result.success()
|
||||||
|
is SyncDataResult.Failure -> {
|
||||||
|
handleSyncFailure(result.message, result.errorAlreadyNotified)
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
notifier.showSyncError(e.message)
|
handleSyncFailure(
|
||||||
Result.success() // try again next time
|
e.message ?: context.stringResource(MR.strings.unknown_error),
|
||||||
|
errorAlreadyNotified = false,
|
||||||
|
)
|
||||||
|
Result.success()
|
||||||
} finally {
|
} finally {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
syncStatus.stop()
|
syncStatus.stop()
|
||||||
|
|
@ -78,10 +92,19 @@ class SyncDataJob(private val context: Context, workerParams: WorkerParameters)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSyncFailure(message: String, errorAlreadyNotified: Boolean) {
|
||||||
|
if (tags.contains(TAG_BEFORE_LIBRARY_UPDATE)) {
|
||||||
|
LibraryUpdateSyncResult.setFailure(message)
|
||||||
|
} else if (!errorAlreadyNotified) {
|
||||||
|
notifier.showSyncError(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG_JOB = "SyncDataJob"
|
private const val TAG_JOB = "SyncDataJob"
|
||||||
private const val TAG_AUTO = "$TAG_JOB:auto"
|
private const val TAG_AUTO = "$TAG_JOB:auto"
|
||||||
const val TAG_MANUAL = "$TAG_JOB:manual"
|
const val TAG_MANUAL = "$TAG_JOB:manual"
|
||||||
|
const val TAG_BEFORE_LIBRARY_UPDATE = "$TAG_JOB:before-library-update"
|
||||||
|
|
||||||
fun isRunning(context: Context): Boolean {
|
fun isRunning(context: Context): Boolean {
|
||||||
return context.workManager.isRunning(TAG_JOB)
|
return context.workManager.isRunning(TAG_JOB)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package eu.kanade.tachiyomi.data.sync
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures the most recent sync failure reported by a [eu.kanade.tachiyomi.data.sync.service.SyncService]
|
||||||
|
* implementation so [SyncManager] can propagate the message without losing context.
|
||||||
|
*/
|
||||||
|
internal object SyncFailureState {
|
||||||
|
private var failure: Pair<String, Boolean>? = null
|
||||||
|
|
||||||
|
fun report(message: String, errorAlreadyNotified: Boolean = true) {
|
||||||
|
failure = message to errorAlreadyNotified
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(): Pair<String, Boolean>? = failure.also { failure = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SyncDataResult {
|
||||||
|
data object Success : SyncDataResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param message Human-readable failure reason.
|
||||||
|
* @param errorAlreadyNotified `true` when a sync service already showed a sync error notification.
|
||||||
|
*/
|
||||||
|
data class Failure(
|
||||||
|
val message: String,
|
||||||
|
val errorAlreadyNotified: Boolean = false,
|
||||||
|
) : SyncDataResult
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,9 @@ import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import logcat.logcat
|
import logcat.logcat
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.data.Chapters
|
import tachiyomi.data.Chapters
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.data.manga.MangaMapper.mapManga
|
import tachiyomi.data.manga.MangaMapper.mapManga
|
||||||
|
|
@ -72,7 +74,9 @@ class SyncManager(
|
||||||
* This function retrieves local data (favorites, manga, extensions, and categories)
|
* This function retrieves local data (favorites, manga, extensions, and categories)
|
||||||
* from the database using the BackupManager, then synchronizes the data with a sync service.
|
* from the database using the BackupManager, then synchronizes the data with a sync service.
|
||||||
*/
|
*/
|
||||||
suspend fun syncData() {
|
suspend fun syncData(): SyncDataResult {
|
||||||
|
SyncFailureState.consume()
|
||||||
|
|
||||||
// Reset isSyncing in case it was left over or failed syncing during restore.
|
// Reset isSyncing in case it was left over or failed syncing during restore.
|
||||||
handler.await(inTransaction = true) {
|
handler.await(inTransaction = true) {
|
||||||
mangasQueries.resetIsSyncing()
|
mangasQueries.resetIsSyncing()
|
||||||
|
|
@ -153,12 +157,22 @@ class SyncManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val remoteBackup = syncService?.doSync(syncData)
|
if (syncService == null) {
|
||||||
|
return SyncDataResult.Failure(
|
||||||
|
message = context.stringResource(MR.strings.sync_failed_generic),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val remoteBackup = syncService.doSync(syncData)
|
||||||
|
|
||||||
if (remoteBackup == null) {
|
if (remoteBackup == null) {
|
||||||
logcat(LogPriority.DEBUG) { "Skip restore due to network issues" }
|
logcat(LogPriority.DEBUG) { "Skip restore due to sync failure" }
|
||||||
// should we call showSyncError?
|
val (message, errorAlreadyNotified) = SyncFailureState.consume()
|
||||||
return
|
?: (context.stringResource(MR.strings.sync_failed_generic) to false)
|
||||||
|
return SyncDataResult.Failure(
|
||||||
|
message = message,
|
||||||
|
errorAlreadyNotified = errorAlreadyNotified,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteBackup === syncData.backup) {
|
if (remoteBackup === syncData.backup) {
|
||||||
|
|
@ -166,13 +180,14 @@ class SyncManager(
|
||||||
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
|
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
|
||||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
notifier.showSyncSuccess("Sync completed successfully")
|
notifier.showSyncSuccess("Sync completed successfully")
|
||||||
return
|
return SyncDataResult.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the sync early if the remote backup is null or empty
|
// Stop the sync early if the remote backup is null or empty
|
||||||
if (remoteBackup.backupManga.isEmpty()) {
|
if (remoteBackup.backupManga.isEmpty()) {
|
||||||
notifier.showSyncError("No data found on remote server.")
|
val message = "No data found on remote server."
|
||||||
return
|
notifier.showSyncError(message)
|
||||||
|
return SyncDataResult.Failure(message = message, errorAlreadyNotified = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's first sync based on lastSyncTimestamp
|
// Check if it's first sync based on lastSyncTimestamp
|
||||||
|
|
@ -180,7 +195,7 @@ class SyncManager(
|
||||||
// It's first sync no need to restore data. (just update remote data)
|
// It's first sync no need to restore data. (just update remote data)
|
||||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
notifier.showSyncSuccess("Updated remote data successfully")
|
notifier.showSyncSuccess("Updated remote data successfully")
|
||||||
return
|
return SyncDataResult.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup)
|
val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup)
|
||||||
|
|
@ -208,7 +223,7 @@ class SyncManager(
|
||||||
// update the sync timestamp
|
// update the sync timestamp
|
||||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
notifier.showSyncSuccess("Sync completed successfully")
|
notifier.showSyncSuccess("Sync completed successfully")
|
||||||
return
|
return SyncDataResult.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupUri = writeSyncDataToCache(context, newSyncData)
|
val backupUri = writeSyncDataToCache(context, newSyncData)
|
||||||
|
|
@ -228,8 +243,12 @@ class SyncManager(
|
||||||
|
|
||||||
// update the sync timestamp
|
// update the sync timestamp
|
||||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||||
|
return SyncDataResult.Success
|
||||||
} else {
|
} else {
|
||||||
logcat(LogPriority.ERROR) { "Failed to write sync data to file" }
|
logcat(LogPriority.ERROR) { "Failed to write sync data to file" }
|
||||||
|
return SyncDataResult.Failure(
|
||||||
|
message = context.stringResource(MR.strings.sync_failed_generic),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import com.google.api.services.drive.DriveScopes
|
||||||
import com.google.api.services.drive.model.File
|
import com.google.api.services.drive.model.File
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.sync.SyncFailureState
|
||||||
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
|
@ -63,6 +65,8 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||||
|
|
||||||
private val googleDriveService = GoogleDriveService(context)
|
private val googleDriveService = GoogleDriveService(context)
|
||||||
|
|
||||||
|
private val notifier = SyncNotifier(context)
|
||||||
|
|
||||||
private val protoBuf: ProtoBuf = Injekt.get()
|
private val protoBuf: ProtoBuf = Injekt.get()
|
||||||
|
|
||||||
override suspend fun doSync(syncData: SyncData): Backup? {
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
|
|
@ -96,7 +100,10 @@ class GoogleDriveSyncService(context: Context, json: Json, syncPreferences: Sync
|
||||||
pushSyncData(syncData)
|
pushSyncData(syncData)
|
||||||
return syncData.backup
|
return syncData.backup
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, "SyncService") { "Error syncing: ${e.message}" }
|
val message = e.message ?: context.stringResource(MR.strings.unknown_error)
|
||||||
|
logcat(LogPriority.ERROR, "SyncService") { "Error syncing: $message" }
|
||||||
|
notifier.showSyncError(message)
|
||||||
|
SyncFailureState.report(message)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.sync.service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.sync.SyncFailureState
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
|
@ -87,10 +88,11 @@ class SyncYomiSyncService(
|
||||||
if (success) {
|
if (success) {
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||||
} else {
|
} else {
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
val message = "Failed to push sync data"
|
||||||
// KMK -->
|
reportSyncEvent(SyncEventStatus.SYNC_FAILED, message)
|
||||||
|
notifier.showSyncError(message)
|
||||||
|
SyncFailureState.report(message)
|
||||||
return null
|
return null
|
||||||
// KMK <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalSyncData.backup
|
return finalSyncData.backup
|
||||||
|
|
@ -99,9 +101,11 @@ class SyncYomiSyncService(
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
val message = e.message ?: context.stringResource(MR.strings.unknown_error)
|
||||||
notifier.showSyncError(e.message)
|
logcat(LogPriority.ERROR) { "Error syncing: $message" }
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
notifier.showSyncError(message)
|
||||||
|
SyncFailureState.report(message)
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_ERROR, message)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.sync.service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.sync.SyncFailureState
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
|
|
@ -117,7 +118,9 @@ class WebDavSyncService(
|
||||||
return finalSyncData.backup
|
return finalSyncData.backup
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
xLogE("WebDAV sync error:", e)
|
xLogE("WebDAV sync error:", e)
|
||||||
notifier.showSyncError(e.message)
|
val message = e.message ?: context.stringResource(MR.strings.unknown_error)
|
||||||
|
notifier.showSyncError(message)
|
||||||
|
SyncFailureState.report(message)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -182,6 +185,7 @@ class WebDavSyncService(
|
||||||
"Please retry syncing or check your WebDAV folder."
|
"Please retry syncing or check your WebDAV folder."
|
||||||
xLogW(message)
|
xLogW(message)
|
||||||
notifier.showSyncError(message)
|
notifier.showSyncError(message)
|
||||||
|
SyncFailureState.report(message)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val bodyStr = response.body.string()
|
val bodyStr = response.body.string()
|
||||||
|
|
|
||||||
|
|
@ -923,6 +923,8 @@
|
||||||
<string name="notification_chapters_single_and_more">Chapter %1$s and %2$d more</string>
|
<string name="notification_chapters_single_and_more">Chapter %1$s and %2$d more</string>
|
||||||
<string name="notification_chapters_multiple">Chapters %1$s</string>
|
<string name="notification_chapters_multiple">Chapters %1$s</string>
|
||||||
<string name="notification_update_error">%1$d update(s) failed</string>
|
<string name="notification_update_error">%1$d update(s) failed</string>
|
||||||
|
<string name="library_update_skipped_sync_error">Library update skipped: %1$s</string>
|
||||||
|
<string name="sync_failed_generic">Could not sync with the remote service</string>
|
||||||
<string name="learn_more">Tap to learn more</string>
|
<string name="learn_more">Tap to learn more</string>
|
||||||
<string name="notification_cover_update_failed">Failed to update cover</string>
|
<string name="notification_cover_update_failed">Failed to update cover</string>
|
||||||
<string name="notification_first_add_to_library">Please add the entry to your library before doing this</string>
|
<string name="notification_first_add_to_library">Please add the entry to your library before doing this</string>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue