diff --git a/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt b/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt index 058e53c09..b200b051e 100644 --- a/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt +++ b/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt @@ -55,7 +55,7 @@ class StrictModeNotifier( .setContentIntent( PendingIntentCompat.getActivity( context, - 0, + violation.hashCode(), ShareHelper(context).getShareTextIntent(violation.stackTraceToString()), 0, false, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 610785d48..891ebe7d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -280,6 +280,10 @@ + ?): CompositeResult { val result = CompositeResult() - for (item in entry.data.asTypedList()) { + val list = entry.data.asTypedList() + outProgress?.emit(Progress(progress = 0, total = list.size)) + for ((index, item) in list.withIndex()) { val mangaJson = item.getJSONObject("manga") val manga = JsonDeserializer(mangaJson).toMangaEntity() val tags = mangaJson.getJSONArray("tags").mapJSON { @@ -144,6 +148,7 @@ class BackupRepository @Inject constructor( db.getHistoryDao().upsert(history) } } + outProgress?.emit(Progress(progress = index, total = list.size)) } return result } @@ -159,9 +164,11 @@ class BackupRepository @Inject constructor( return result } - suspend fun restoreFavourites(entry: BackupEntry): CompositeResult { + suspend fun restoreFavourites(entry: BackupEntry, outProgress: FlowCollector?): CompositeResult { val result = CompositeResult() - for (item in entry.data.asTypedList()) { + val list = entry.data.asTypedList() + outProgress?.emit(Progress(progress = 0, total = list.size)) + for ((index, item) in list.withIndex()) { val mangaJson = item.getJSONObject("manga") val manga = JsonDeserializer(mangaJson).toMangaEntity() val tags = mangaJson.getJSONArray("tags").mapJSON { @@ -175,6 +182,7 @@ class BackupRepository @Inject constructor( db.getFavouritesDao().upsert(favourite) } } + outProgress?.emit(Progress(progress = index, total = list.size)) } return result } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/CompositeResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/CompositeResult.kt index 9311bb253..4f1ba1ca2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/CompositeResult.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/CompositeResult.kt @@ -27,6 +27,10 @@ class CompositeResult { } } + operator fun plusAssign(error: Throwable) { + errors.add(error) + } + operator fun plusAssign(other: CompositeResult) { this.successCount += other.successCount this.errors += other.errors diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt index 63663e9b9..4c81e9efb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt @@ -54,6 +54,7 @@ import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet import org.koitharu.kotatsu.list.ui.config.ListConfigSection import org.koitharu.kotatsu.local.ui.ImportDialogFragment import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog +import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaListFilter @@ -512,6 +513,8 @@ class AppRouter private constructor( fun suggestionsIntent(context: Context) = Intent(context, SuggestionsActivity::class.java) + fun homeIntent(context: Context) = Intent(context, MainActivity::class.java) + fun mangaUpdatesIntent(context: Context) = Intent(context, UpdatesActivity::class.java) fun readerSettingsIntent(context: Context) = @@ -561,9 +564,14 @@ class AppRouter private constructor( .putExtra(KEY_SOURCE, source.name) } + const val KEY_DATA = "data" + const val KEY_ENTRIES = "entries" + const val KEY_ERROR = "error" const val KEY_EXCLUDE = "exclude" + const val KEY_FILE = "file" const val KEY_FILTER = "filter" const val KEY_ID = "id" + const val KEY_INDEX = "index" const val KEY_LIST_SECTION = "list_section" const val KEY_MANGA = "manga" const val KEY_MANGA_LIST = "manga_list" @@ -573,12 +581,8 @@ class AppRouter private constructor( const val KEY_SOURCE = "source" const val KEY_TAB = "tab" const val KEY_TITLE = "title" - const val KEY_USER_AGENT = "user_agent" const val KEY_URL = "url" - const val KEY_ERROR = "error" - const val KEY_FILE = "file" - const val KEY_INDEX = "index" - const val KEY_DATA = "data" + const val KEY_USER_AGENT = "user_agent" const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY" const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt index 04c4c7eb6..31316de80 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/CoroutineIntentService.kt @@ -14,6 +14,7 @@ import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -42,6 +43,8 @@ abstract class CoroutineIntentService : BaseService() { intentJobContext.processIntent(intent) } } + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { e.printStackTraceDebug() intentJobContext.onError(e) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ContentResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ContentResolver.kt index 883df5b5a..3bba8b0b2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ContentResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/ContentResolver.kt @@ -1,11 +1,14 @@ package org.koitharu.kotatsu.core.util.ext -import android.annotation.TargetApi +import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Build import android.os.storage.StorageManager import android.provider.DocumentsContract +import android.provider.OpenableColumns +import androidx.annotation.RequiresApi +import androidx.core.net.toFile import org.koitharu.kotatsu.parsers.util.nullIfEmpty import org.koitharu.kotatsu.parsers.util.removeSuffix import java.io.File @@ -31,6 +34,21 @@ fun Uri.resolveFile(context: Context): File? { ) } +fun ContentResolver.getFileDisplayName(uri: Uri): String? = runCatching { + if (uri.isFileUri()) { + return@runCatching uri.toFile().name + } + query(uri, null, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + } else { + null + } + } +}.onFailure { e -> + e.printStackTraceDebug() +}.getOrNull() + private fun getVolumePath(volumeId: String, context: Context): String? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getVolumePathForAndroid11AndAbove(volumeId, context) @@ -63,7 +81,7 @@ private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): St it.printStackTraceDebug() }.getOrNull() -@TargetApi(Build.VERSION_CODES.R) +@RequiresApi(Build.VERSION_CODES.R) private fun getVolumePathForAndroid11AndAbove(volumeId: String, context: Context): String? = runCatching { val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager storageManager.storageVolumes.firstNotNullOfOrNull { volume -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/Progress.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/Progress.kt new file mode 100644 index 000000000..503c76e73 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/progress/Progress.kt @@ -0,0 +1,47 @@ +package org.koitharu.kotatsu.core.util.progress + +data class Progress( + val progress: Int, + val total: Int, +) : Comparable { + + val percent: Float + get() = if (total == 0) 0f else progress / total.toFloat() + + val isEmpty: Boolean + get() = progress == 0 + + val isFull: Boolean + get() = progress == total + + override fun compareTo(other: Progress): Int = if (total == other.total) { + progress.compareTo(other.progress) + } else { + percent.compareTo(other.percent) + } + + operator fun inc() = if (isFull) { + this + } else { + copy( + progress = progress + 1, + total = total, + ) + } + + operator fun dec() = if (isEmpty) { + this + } else { + copy( + progress = progress - 1, + total = total, + ) + } + + operator fun plus(child: Progress) = Progress( + progress = progress * child.total + child.progress, + total = total * child.total, + ) + + fun percentSting() = (percent * 100f).toInt().toString() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt index 61b64637a..cef00b6f4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt @@ -97,9 +97,9 @@ class AppBackupAgent : BackupAgent() { } try { runBlocking { - backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it) } + backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it, null) } backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) } - backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it) } + backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it, null) } backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) } backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) } backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt index 330eb9e05..efa040479 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreDialogFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.viewModels @@ -11,7 +12,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.backup.CompositeResult import org.koitharu.kotatsu.core.nav.router import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener @@ -23,7 +23,6 @@ import org.koitharu.kotatsu.databinding.DialogRestoreBinding import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date -import kotlin.math.roundToInt @AndroidEntryPoint class RestoreDialogFragment : AlertDialogFragment(), OnListItemClickListener, @@ -43,8 +42,6 @@ class RestoreDialogFragment : AlertDialogFragment(), OnLis binding.buttonCancel.setOnClickListener(this) binding.buttonRestore.setOnClickListener(this) viewModel.availableEntries.observe(viewLifecycleOwner, adapter) - viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged) - viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone) viewModel.onError.observeEvent(viewLifecycleOwner, this::onError) combine( viewModel.isLoading, @@ -63,7 +60,15 @@ class RestoreDialogFragment : AlertDialogFragment(), OnLis override fun onClick(v: View) { when (v.id) { R.id.button_cancel -> dismiss() - R.id.button_restore -> viewModel.restore() + R.id.button_restore -> { + if (startRestoreService()) { + Toast.makeText(v.context, R.string.backup_restored_background, Toast.LENGTH_SHORT).show() + router.closeWelcomeSheet() + dismiss() + } else { + Toast.makeText(v.context, R.string.operation_not_supported, Toast.LENGTH_SHORT).show() + } + } } } @@ -87,6 +92,14 @@ class RestoreDialogFragment : AlertDialogFragment(), OnLis } } + private fun startRestoreService(): Boolean { + return RestoreService.start( + context ?: return false, + viewModel.uri ?: return false, + viewModel.getCheckedEntries(), + ) + } + private fun Date.formatBackupDate(): String { return getString( R.string.backup_date_, @@ -102,46 +115,4 @@ class RestoreDialogFragment : AlertDialogFragment(), OnLis .show() dismiss() } - - private fun onProgressChanged(value: Float) { - with(requireViewBinding().progressBar) { - isVisible = true - val wasIndeterminate = isIndeterminate - isIndeterminate = value < 0 - if (value >= 0) { - setProgressCompat((value * max).roundToInt(), !wasIndeterminate) - } - } - } - - private fun onRestoreDone(result: CompositeResult) { - val builder = MaterialAlertDialogBuilder(context ?: return) - when { - result.isEmpty -> { - builder.setTitle(R.string.data_not_restored) - .setMessage(R.string.data_not_restored_text) - } - - result.isAllSuccess -> { - builder.setTitle(R.string.data_restored) - .setMessage(R.string.data_restored_success) - } - - result.isAllFailed -> builder.setTitle(R.string.error) - .setMessage( - result.failures.map { - it.getDisplayMessage(resources) - }.distinct().joinToString("\n"), - ) - - else -> builder.setTitle(R.string.data_restored) - .setMessage(R.string.data_restored_with_errors) - } - builder.setPositiveButton(android.R.string.ok, null) - .show() - if (!result.isEmpty && !result.isAllFailed) { - router.closeWelcomeSheet() - } - dismiss() - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt new file mode 100644 index 000000000..699690afb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt @@ -0,0 +1,287 @@ +package org.koitharu.kotatsu.settings.backup + +import android.annotation.SuppressLint +import android.app.Notification +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.net.Uri +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.content.ContextCompat +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ErrorReporterReceiver +import org.koitharu.kotatsu.core.backup.BackupEntry +import org.koitharu.kotatsu.core.backup.BackupRepository +import org.koitharu.kotatsu.core.backup.BackupZipInput +import org.koitharu.kotatsu.core.backup.CompositeResult +import org.koitharu.kotatsu.core.nav.AppRouter +import org.koitharu.kotatsu.core.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getFileDisplayName +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.toUriOrNull +import org.koitharu.kotatsu.core.util.progress.Progress +import org.koitharu.kotatsu.parsers.util.mapToArray +import org.koitharu.kotatsu.parsers.util.nullIfEmpty +import java.io.File +import java.io.FileNotFoundException +import java.util.EnumSet +import javax.inject.Inject +import com.google.android.material.R as materialR + +@AndroidEntryPoint +class RestoreService : CoroutineIntentService() { + + @Inject + lateinit var repository: BackupRepository + + private lateinit var notificationManager: NotificationManagerCompat + + override fun onCreate() { + super.onCreate() + notificationManager = NotificationManagerCompat.from(applicationContext) + } + + override suspend fun IntentJobContext.processIntent(intent: Intent) { + startForeground(this) + val uri = intent.getStringExtra(AppRouter.KEY_DATA)?.toUriOrNull() ?: throw FileNotFoundException() + val displayName = contentResolver.getFileDisplayName(uri) + val entries = intent.getIntArrayExtra(AppRouter.KEY_ENTRIES) + ?.mapTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { BackupEntry.Name.entries[it] } + if (entries.isNullOrEmpty()) { + throw IllegalArgumentException("No entries specified") + } + val result = runInterruptible(Dispatchers.IO) { + val tempFile = File.createTempFile("backup_", ".tmp") + (contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input -> + tempFile.outputStream().use { output -> + input.copyTo(output) + } + } + BackupZipInput.from(tempFile) + }.use { backupInput -> + restoreImpl(displayName, backupInput, entries) + } + if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { + val notification = buildNotification(displayName, result) + notificationManager.notify(TAG, startId, notification) + } + } + + override fun IntentJobContext.onError(error: Throwable) { + if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { + val result = CompositeResult() + result += error + val notification = buildNotification(null, result) + notificationManager.notify(TAG, startId, notification) + } + } + + private suspend fun IntentJobContext.restoreImpl( + displayName: String?, + input: BackupZipInput, + entries: Set + ): CompositeResult { + val result = CompositeResult() + val showNotification = applicationContext.checkNotificationPermission(CHANNEL_ID) + var progress = Progress(0, entries.size) + + fun notify(childProgress: Progress? = null) { + if (showNotification) { + val p = childProgress?.let { progress + it } ?: progress + notificationManager.notify(FOREGROUND_NOTIFICATION_ID, buildNotification(displayName, p)) + } + } + + notify() + + if (BackupEntry.Name.HISTORY in entries) { + input.getEntry(BackupEntry.Name.HISTORY)?.let { + flow { + result += repository.restoreHistory(it, this) + }.collect { p -> + notify(p) + } + } + progress++ + } + + notify() + + if (BackupEntry.Name.CATEGORIES in entries) { + input.getEntry(BackupEntry.Name.CATEGORIES)?.let { + result += repository.restoreCategories(it) + } + progress++ + } + + notify() + + if (BackupEntry.Name.FAVOURITES in entries) { + input.getEntry(BackupEntry.Name.FAVOURITES)?.let { + flow { + result += repository.restoreFavourites(it, this) + }.collect { p -> + notify(p) + } + } + } + + notify() + + if (BackupEntry.Name.BOOKMARKS in entries) { + input.getEntry(BackupEntry.Name.BOOKMARKS)?.let { + result += repository.restoreBookmarks(it) + } + progress++ + } + + notify() + + if (BackupEntry.Name.SOURCES in entries) { + input.getEntry(BackupEntry.Name.SOURCES)?.let { + result += repository.restoreSources(it) + } + progress++ + } + + notify() + + if (BackupEntry.Name.SETTINGS in entries) { + input.getEntry(BackupEntry.Name.SETTINGS)?.let { + result += repository.restoreSettings(it) + } + progress++ + } + + notify() + + return result + } + + @SuppressLint("InlinedApi") + private fun startForeground(jobContext: IntentJobContext) { + val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW) + .setName(getString(R.string.restoring_backup)) + .setShowBadge(true) + .setVibrationEnabled(false) + .setSound(null, null) + .setLightsEnabled(false) + .build() + notificationManager.createNotificationChannel(channel) + + val notification = jobContext.buildNotification(null, null) + + jobContext.setForeground( + FOREGROUND_NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, + ) + } + + private fun IntentJobContext.buildNotification(fileName: String?, progress: Progress?): Notification { + return NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setContentTitle(getString(R.string.restoring_backup)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setDefaults(0) + .setSilent(true) + .setOngoing(true) + .setProgress(progress?.total ?: 0, progress?.progress ?: 0, progress == null) + .setContentText( + concatStrings( + context = this@RestoreService, + a = fileName, + b = progress?.run { getString(R.string.percent_string_pattern, percentSting()) }, + ), + ) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .addAction( + materialR.drawable.material_ic_clear_black_24dp, + applicationContext.getString(android.R.string.cancel), + getCancelIntent(), + ).build() + } + + private fun buildNotification(fileName: String?, result: CompositeResult): Notification { + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setDefaults(0) + .setSilent(true) + .setAutoCancel(true) + .setSubText(fileName) + + when { + result.isEmpty -> notification.setContentTitle(getString(R.string.data_not_restored)) + .setContentText(getString(R.string.data_not_restored_text)) + .setSmallIcon(android.R.drawable.stat_notify_error) + + result.isAllSuccess -> notification.setContentTitle(getString(R.string.data_restored)) + .setContentText(getString(R.string.data_restored_success)) + .setSmallIcon(R.drawable.ic_stat_done) + + result.isAllFailed -> notification.setContentTitle(getString(R.string.error)) + .setContentText( + result.failures.map { it.getDisplayMessage(resources) }.distinct().joinToString("\n"), + ) + .setSmallIcon(android.R.drawable.stat_notify_error) + + else -> notification.setContentTitle(getString(R.string.data_restored)) + .setContentText(getString(R.string.data_restored_with_errors)) + .setSmallIcon(R.drawable.ic_stat_done) + } + result.failures.firstOrNull()?.let { error -> + ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> + notification.addAction( + R.drawable.ic_alert_outline, + applicationContext.getString(R.string.report), + reportIntent, + ) + } + } + notification.setContentIntent( + PendingIntentCompat.getActivity( + applicationContext, + 0, + AppRouter.homeIntent(this), + 0, + false, + ), + ) + return notification.build() + } + + private fun concatStrings(context: Context, a: String?, b: String?): String? = when { + a.isNullOrEmpty() && b.isNullOrEmpty() -> null + a.isNullOrEmpty() -> b?.nullIfEmpty() + b.isNullOrEmpty() -> a.nullIfEmpty() + else -> context.getString(R.string.download_summary_pattern, a, b) + } + + companion object { + + private const val TAG = "restore" + private const val CHANNEL_ID = "restore_backup" + private const val FOREGROUND_NOTIFICATION_ID = 39 + + fun start(context: Context, uri: Uri, entries: Set): Boolean = try { + val intent = Intent(context, RestoreService::class.java) + intent.putExtra(AppRouter.KEY_DATA, uri.toString()) + intent.putExtra(AppRouter.KEY_ENTRIES, entries.mapToArray { it.ordinal }.toIntArray()) + ContextCompat.startForegroundService(context, intent) + true + } catch (e: Exception) { + e.printStackTraceDebug() + false + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index 67ac872ae..525602579 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -10,14 +10,9 @@ import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.backup.BackupEntry import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupZipInput -import org.koitharu.kotatsu.core.backup.CompositeResult import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.ui.BaseViewModel -import org.koitharu.kotatsu.core.util.ext.MutableEventFlow -import org.koitharu.kotatsu.core.util.ext.call -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.toUriOrNull -import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import java.io.File import java.io.FileNotFoundException import java.util.Date @@ -32,30 +27,28 @@ class RestoreViewModel @Inject constructor( @ApplicationContext context: Context, ) : BaseViewModel() { - private val backupInput = suspendLazy { - val uri = savedStateHandle.get(AppRouter.KEY_FILE) - ?.toUriOrNull() ?: throw FileNotFoundException() - val contentResolver = context.contentResolver + val uri = savedStateHandle.get(AppRouter.KEY_FILE)?.toUriOrNull() + private val contentResolver = context.contentResolver + + val availableEntries = MutableStateFlow>(emptyList()) + val backupDate = MutableStateFlow(null) + + init { + launchLoadingJob(Dispatchers.Default) { + loadBackupInfo() + } + } + + private suspend fun loadBackupInfo() { runInterruptible(Dispatchers.IO) { val tempFile = File.createTempFile("backup_", ".tmp") - (contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input -> + (uri?.let { contentResolver.openInputStream(it) } ?: throw FileNotFoundException()).use { input -> tempFile.outputStream().use { output -> input.copyTo(output) } } BackupZipInput.from(tempFile) - } - } - - val progress = MutableStateFlow(-1f) - val onRestoreDone = MutableEventFlow() - - val availableEntries = MutableStateFlow>(emptyList()) - val backupDate = MutableStateFlow(null) - - init { - launchLoadingJob(Dispatchers.Default) { - val backup = backupInput.get() + }.use { backup -> val entries = backup.entries() availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry -> if (entry == BackupEntry.Name.INDEX || entry !in entries) { @@ -71,15 +64,6 @@ class RestoreViewModel @Inject constructor( } } - override fun onCleared() { - super.onCleared() - runCatching { - backupInput.peek()?.closeAndDelete() - }.onFailure { - it.printStackTraceDebug() - } - } - fun onItemClick(item: BackupEntryModel) { val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name } map[item.name] = item.copy(isChecked = !item.isChecked) @@ -87,61 +71,10 @@ class RestoreViewModel @Inject constructor( availableEntries.value = map.values.sortedBy { it.name.ordinal } } - fun restore() { - launchLoadingJob { - val backup = backupInput.get() - val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { - if (it.isChecked) it.name else null - } - val result = CompositeResult() - val step = 1f / 6f - - progress.value = 0f - if (BackupEntry.Name.HISTORY in checkedItems) { - backup.getEntry(BackupEntry.Name.HISTORY)?.let { - result += repository.restoreHistory(it) - } - } - - progress.value += step - if (BackupEntry.Name.CATEGORIES in checkedItems) { - backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { - result += repository.restoreCategories(it) - } - } - - progress.value += step - if (BackupEntry.Name.FAVOURITES in checkedItems) { - backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { - result += repository.restoreFavourites(it) - } - } - - progress.value += step - if (BackupEntry.Name.BOOKMARKS in checkedItems) { - backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { - result += repository.restoreBookmarks(it) - } - } - - progress.value += step - if (BackupEntry.Name.SOURCES in checkedItems) { - backup.getEntry(BackupEntry.Name.SOURCES)?.let { - result += repository.restoreSources(it) - } - } - - progress.value += step - if (BackupEntry.Name.SETTINGS in checkedItems) { - backup.getEntry(BackupEntry.Name.SETTINGS)?.let { - result += repository.restoreSettings(it) - } - } - - progress.value = 1f - onRestoreDone.call(result) + fun getCheckedEntries(): Set = availableEntries.value + .mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { + if (it.isChecked) it.name else null } - } /** * Check for inconsistent user selection diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a6e919da..f5b4e9b05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,781 +1,781 @@ - Kotatsu - Local storage - Favourites - History - An error occurred - Network error - Details - Chapters - Liste - Detailed list - Grid - List mode - Settings - Manga sources - Loading… - Computing… - Chapter %1$d of %2$d - Close - Try again - + Kotatsu + Local storage + Favourites + History + An error occurred + Network error + Details + Chapters + Liste + Detailed list + Grid + List mode + Settings + Manga sources + Loading… + Computing… + Chapter %1$d of %2$d + Close + Try again + Retry - Clear history - Nothing found - No history yet - Read - No favourites yet - Favourite this - New category - Add - Save - Share - Create shortcut… - Share %s - Search - Search manga - Downloading… - Processing… - Downloaded - Downloads - Name - Popular - Updated - Newest - Rating - Sorting order - Filter - Theme - Light - Dark - + Clear history + Nothing found + No history yet + Read + No favourites yet + Favourite this + New category + Add + Save + Share + Create shortcut… + Share %s + Search + Search manga + Downloading… + Processing… + Downloaded + Downloads + Name + Popular + Updated + Newest + Rating + Sorting order + Filter + Theme + Light + Dark + Follow system - Pages - Clear - Remove - \"%s\" deleted from local storage - Save page - Page saved - Pages saved - Share image - Import - Delete - This operation is not supported - Either pick a ZIP or CBZ file. - No description - Clear page cache - B|kB|MB|GB|TB - Standard - Webtoon - Read mode - Grid size - Search on %s - Delete manga - Permanently delete \"%s\" from device? - Reader settings - Switch pages - Continue - Error - Clear thumbnails cache - Clear search history - Cleared - Internal storage - External storage - Domain - A new version of the app is available - Open in web browser - Notifications - %1$d of %2$d on - New chapters - Download - Notifications settings - Notification sound - LED indicator - Vibration - Favourite categories - Remove - It\'s kind of empty here… - Try to reformulate the query. - What you read will be displayed here - Find what to read in the «Explore» section - There are no manga matching the filters you selected - Save something first - Save something from an online catalog or import it from a file. - Shelf - Recent - Page animation - Downloads folder - Not available - No available storage - Other storage - Done - All favourites - Empty category - Read later - Updates - New chapters of what you are reading are shown here - Search results - New version: %s - Size: %s - Clear updates feed - Cleared - Rotate screen - Update - Feed update will start soon - Look for updates - Don\'t check - Enter password - Wrong password - Protect the app - Ask for password when starting Kotatsu - Repeat the password - Mismatching passwords - About - Version %s - Check for updates - No updates available - Right-to-left - New category - Scale mode - Fit center - Fit to height - Fit to width - Keep at start - Black - Uses less power on AMOLED screens - Backup and restore - Create data backup - Restore from backup - Restored - Preparing… - File not found - All data was restored - The data was restored, but there are errors - You can create backup of your history and favourites and restore it - Just now - Yesterday - Long ago - Group - Today - Tap to try again - The chosen configuration will be remembered for this manga - Silent - CAPTCHA required - Solve - Clear cookies - All cookies were removed - Clear feed - Clear all update history permanently? - Check for new chapters - Reverse - Grid view - Sign in - Sign in to view this content - Default: %s - Next - Enter a password to start the app with - Confirm - The password must be 4 characters or more - Remove all recent search queries permanently? - Welcome - Backup saved - Some devices have different system behavior, which may break background tasks. - Read more - Queued - The chapter is missing - Translate this app - Translation - Authorized - Logging in on %s is not supported - You will be logged out from all sources - Genres - Finished - Ongoing - Default - Exclude NSFW manga from history - Numbered pages - Screenshot policy - Allow - Block on NSFW - Always block - Suggestions - Enable suggestions - Suggest manga based on your preferences - All data is only analyzed locally on this device and never sent anywhere. - Start reading manga and you will get personalized suggestions - Do not suggest NSFW manga - Enabled - Disabled - Reset filter - Select languages which you want to read manga. You can change it later in settings. - Never - Only on Wi-Fi - Always - Preload pages - Logged in as %s - 18+ - Various languages - Find chapter - No chapters in this manga - %1$s%% - Appearance - Suggestions updating - Exclude genres - Specify genres that you do not want to see in the suggestions - Delete selected items from device permanently? - Removal completed - Shikimori - AniList - Download slowdown - Helps avoid blocking your IP address - Saved manga processing - Chapters will be removed in the background - Canceled - Account already exists - Back - Synchronization - Sync your data - Enter your email to continue - Hide - New manga sources are available - Check for new chapters and notify about it - You will receive notifications about updates of manga you are reading - You will not receive notifications but new chapters will be highlighted in the lists - Enable notifications - Name - Edit - Edit category - Tracking - No favourite categories - Log out - Add bookmark - Remove bookmark - Bookmarks - Bookmark removed - Bookmark added - Undo - Removed from history - DNS over HTTPS - Default mode - Autodetect reader mode - Automatically detect if manga is webtoon - Disable battery optimization - Helps with background updates checks - Something went wrong. Please submit a bug report to the developers to help us fix it. - Send - Planned - Reading - Re-reading - Completed - On hold - Dropped - Disable all - Use biometric if available - Manga from your favourites - Your recently read manga - Report - Show reading progress indicators - Data deletion - Show percentage read in history and favourites - Manga marked as NSFW will never be added to the history and your progress will not be saved - Can help in case of some issues. All authorizations will be invalidated - Show all - Invalid domain - Invalid server address - Select range - Clear all history - Last 2 hours - History cleared - Manage - No bookmarks yet - You can create bookmark while reading manga - Bookmarks removed - No manga sources - Enable manga sources to read manga online - Random - Are you sure you want to delete the selected favorite categories?\nAll manga in it will be lost and this cannot be undone. - Reorder - Empty - Explore - Press "Back" again to exit - Press "Back" twice to exit the app - Exit confirmation - Saved manga - Pages cache - Other cache - Storage usage - Available - %1$s - %2$s - Removed from favourites - Options - Content not found or removed - %1$s · %2$s - Incognito mode - No chapters - Automatic scroll - Ch. %1$d/%2$d Pg. %3$d/%4$d - Show information bar in reader - Comics archive - Folder with images - Importing manga - Import completed - You can delete the original file from storage to save space - Import will start soon - Feed - Error details:<br><tt>%1$s</tt><br><br>1. Try to <a href="%2$s">open manga in a web browser</a> to ensure it is available on its source<br>2. Make sure you are using the <a href="kotatsu://about">latest version of Kotatsu</a><br>3. If it is available, send an error report to the developers. - Show recent manga shortcuts - Make recent manga available by long pressing on application icon - Do not adjust the page switching direction to the reader mode, e. g. pressing the right key always switches to the next page. This option affects only hardware input devices - Ergonomic reader control - Color correction - Brightness - Contrast - Reset - Save or discard unsaved changes\? - Discard - No space left on device - Show page switching slider - Webtoon zoom - Network is not available - Turn on Wi-Fi or mobile network to read manga online - Server side error (%1$d). Please try again later - Also clear information about new chapters - Compact - MyAnimeList - Source disabled - Content preloading - Mark as current - Language - Share logs - Enable logging - Record some actions for debug purposes. Don\'t turn it on if you\'re not sure what you\'re doing - Show suspicious content - Dynamic - Color scheme - Show in grid view - Miku - Asuka - Mion - Rikka - Sakura - Mamimi - Kanade - There is nothing here - To track reading progress, select Menu → Track on the manga details screen. - Services - Allow unstable updates - Receive notifications about unstable builds - Download started - Got it - Tap and hold on an item to reorder them - UserAgent header - Please restart the application to apply these changes - You can select one or more .cbz or .zip files, each file will be recognized as a separate manga. - You can select a directory with archives or images. Each archive (or subdirectory) will be recognized as a chapter. - Speed - Show on the Shelf - You can sign in into an existing account or create a new one - Find similar - Synchronization settings - Server address - You can use a self-hosted synchronization server or a default one. Don\'t change this if you\'re not sure what you\'re doing. - Ignore SSL errors - Choose mirror automatically - Automatically switch domains for manga sources on errors if mirrors are available - Pause - Resume - Paused - + Pages + Clear + Remove + \"%s\" deleted from local storage + Save page + Page saved + Pages saved + Share image + Import + Delete + This operation is not supported + Either pick a ZIP or CBZ file. + No description + Clear page cache + B|kB|MB|GB|TB + Standard + Webtoon + Read mode + Grid size + Search on %s + Delete manga + Permanently delete \"%s\" from device? + Reader settings + Switch pages + Continue + Error + Clear thumbnails cache + Clear search history + Cleared + Internal storage + External storage + Domain + A new version of the app is available + Open in web browser + Notifications + %1$d of %2$d on + New chapters + Download + Notifications settings + Notification sound + LED indicator + Vibration + Favourite categories + Remove + It\'s kind of empty here… + Try to reformulate the query. + What you read will be displayed here + Find what to read in the «Explore» section + There are no manga matching the filters you selected + Save something first + Save something from an online catalog or import it from a file. + Shelf + Recent + Page animation + Downloads folder + Not available + No available storage + Other storage + Done + All favourites + Empty category + Read later + Updates + New chapters of what you are reading are shown here + Search results + New version: %s + Size: %s + Clear updates feed + Cleared + Rotate screen + Update + Feed update will start soon + Look for updates + Don\'t check + Enter password + Wrong password + Protect the app + Ask for password when starting Kotatsu + Repeat the password + Mismatching passwords + About + Version %s + Check for updates + No updates available + Right-to-left + New category + Scale mode + Fit center + Fit to height + Fit to width + Keep at start + Black + Uses less power on AMOLED screens + Backup and restore + Create data backup + Restore from backup + Restored + Preparing… + File not found + All data was restored + The data was restored, but there are errors + You can create backup of your history and favourites and restore it + Just now + Yesterday + Long ago + Group + Today + Tap to try again + The chosen configuration will be remembered for this manga + Silent + CAPTCHA required + Solve + Clear cookies + All cookies were removed + Clear feed + Clear all update history permanently? + Check for new chapters + Reverse + Grid view + Sign in + Sign in to view this content + Default: %s + Next + Enter a password to start the app with + Confirm + The password must be 4 characters or more + Remove all recent search queries permanently? + Welcome + Backup saved + Some devices have different system behavior, which may break background tasks. + Read more + Queued + The chapter is missing + Translate this app + Translation + Authorized + Logging in on %s is not supported + You will be logged out from all sources + Genres + Finished + Ongoing + Default + Exclude NSFW manga from history + Numbered pages + Screenshot policy + Allow + Block on NSFW + Always block + Suggestions + Enable suggestions + Suggest manga based on your preferences + All data is only analyzed locally on this device and never sent anywhere. + Start reading manga and you will get personalized suggestions + Do not suggest NSFW manga + Enabled + Disabled + Reset filter + Select languages which you want to read manga. You can change it later in settings. + Never + Only on Wi-Fi + Always + Preload pages + Logged in as %s + 18+ + Various languages + Find chapter + No chapters in this manga + %1$s%% + Appearance + Suggestions updating + Exclude genres + Specify genres that you do not want to see in the suggestions + Delete selected items from device permanently? + Removal completed + Shikimori + AniList + Download slowdown + Helps avoid blocking your IP address + Saved manga processing + Chapters will be removed in the background + Canceled + Account already exists + Back + Synchronization + Sync your data + Enter your email to continue + Hide + New manga sources are available + Check for new chapters and notify about it + You will receive notifications about updates of manga you are reading + You will not receive notifications but new chapters will be highlighted in the lists + Enable notifications + Name + Edit + Edit category + Tracking + No favourite categories + Log out + Add bookmark + Remove bookmark + Bookmarks + Bookmark removed + Bookmark added + Undo + Removed from history + DNS over HTTPS + Default mode + Autodetect reader mode + Automatically detect if manga is webtoon + Disable battery optimization + Helps with background updates checks + Something went wrong. Please submit a bug report to the developers to help us fix it. + Send + Planned + Reading + Re-reading + Completed + On hold + Dropped + Disable all + Use biometric if available + Manga from your favourites + Your recently read manga + Report + Show reading progress indicators + Data deletion + Show percentage read in history and favourites + Manga marked as NSFW will never be added to the history and your progress will not be saved + Can help in case of some issues. All authorizations will be invalidated + Show all + Invalid domain + Invalid server address + Select range + Clear all history + Last 2 hours + History cleared + Manage + No bookmarks yet + You can create bookmark while reading manga + Bookmarks removed + No manga sources + Enable manga sources to read manga online + Random + Are you sure you want to delete the selected favorite categories?\nAll manga in it will be lost and this cannot be undone. + Reorder + Empty + Explore + Press "Back" again to exit + Press "Back" twice to exit the app + Exit confirmation + Saved manga + Pages cache + Other cache + Storage usage + Available + %1$s - %2$s + Removed from favourites + Options + Content not found or removed + %1$s · %2$s + Incognito mode + No chapters + Automatic scroll + Ch. %1$d/%2$d Pg. %3$d/%4$d + Show information bar in reader + Comics archive + Folder with images + Importing manga + Import completed + You can delete the original file from storage to save space + Import will start soon + Feed + Error details:<br><tt>%1$s</tt><br><br>1. Try to <a href="%2$s">open manga in a web browser</a> to ensure it is available on its source<br>2. Make sure you are using the <a href="kotatsu://about">latest version of Kotatsu</a><br>3. If it is available, send an error report to the developers. + Show recent manga shortcuts + Make recent manga available by long pressing on application icon + Do not adjust the page switching direction to the reader mode, e. g. pressing the right key always switches to the next page. This option affects only hardware input devices + Ergonomic reader control + Color correction + Brightness + Contrast + Reset + Save or discard unsaved changes\? + Discard + No space left on device + Show page switching slider + Webtoon zoom + Network is not available + Turn on Wi-Fi or mobile network to read manga online + Server side error (%1$d). Please try again later + Also clear information about new chapters + Compact + MyAnimeList + Source disabled + Content preloading + Mark as current + Language + Share logs + Enable logging + Record some actions for debug purposes. Don\'t turn it on if you\'re not sure what you\'re doing + Show suspicious content + Dynamic + Color scheme + Show in grid view + Miku + Asuka + Mion + Rikka + Sakura + Mamimi + Kanade + There is nothing here + To track reading progress, select Menu → Track on the manga details screen. + Services + Allow unstable updates + Receive notifications about unstable builds + Download started + Got it + Tap and hold on an item to reorder them + UserAgent header + Please restart the application to apply these changes + You can select one or more .cbz or .zip files, each file will be recognized as a separate manga. + You can select a directory with archives or images. Each archive (or subdirectory) will be recognized as a chapter. + Speed + Show on the Shelf + You can sign in into an existing account or create a new one + Find similar + Synchronization settings + Server address + You can use a self-hosted synchronization server or a default one. Don\'t change this if you\'re not sure what you\'re doing. + Ignore SSL errors + Choose mirror automatically + Automatically switch domains for manga sources on errors if mirrors are available + Pause + Resume + Paused + Remove completed - Cancel all - Download only via Wi-Fi - Stop downloading when switching to a mobile network - Suggestion: %s - Sometimes show notifications with suggested manga - More - Enable - No thanks - All active downloads will be cancelled, partially downloaded data will be lost - Your downloads history will be permanently deleted. No downloaded files will be affected - You don\'t have any downloads - Downloads have been resumed - Downloads have been paused - Downloads have been removed - Downloads have been cancelled - Do you want to receive personalized manga suggestions? - WebView not available: check if WebView provider is installed - Clear network cache - Type - Address - Port - Proxy - Invalid value - Kitsu - Enter your email and password to continue - Downloaded - Images optimization proxy - Use the wsrv.nl service to reduce traffic usage and speed up image loading if possible - Invert colors - Username - Password - Authorization (optional) - Invalid port number - Network - Data and privacy - Restore previously created backup - Allow zoom in gesture in webtoon mode - Show the current time and reading progress at the top of the screen - Show page numbers in bottom corner - Clear cookies for specified domain only. In most cases will invalidate authorization - All chapters with translation %s - The whole manga - First %s - Next unread %s - All unread chapters - All unread chapters (%s) - Select chapters manually - Pick custom directory - You have no access to this file or directory - Local manga directories - Description - This month - Voice search - Related manga - Light - Dark - White - Black - Background - Data was not restored - Make sure you have selected the correct backup file - Manage categories - Do not update suggestions using metered network connections - Do not check for new chapters using metered network connections - Enter manga title, genre or source name - Progress - Added - Show - %s requires a captcha to be resolved to work properly - Languages - Unknown - In progress - Disable NSFW - Too many requests. Try again later - Too many requests. Try again after %s - Show a list of related manga. In some cases it may be inaccurate or missing - Advanced - Manga list - Invalid data is returned or file is corrupted - On device - Directories - Main screen sections - No more items can be added - To top - Moved to top - Zoom out - Zoom in - Show zoom buttons - Whether to show zoom control buttons in the bottom right corner - Keep screen on - Do not turn the screen off while you\'re reading manga - Dropped - Reduces banding, but may impact performance - 32-bit color mode - Suggest new sources after app update - Prompt to enable newly added sources after updating the application - List options - Relevance - Categories - Online variant - Periodic backups - Backup creation frequency - Every day - Every 2 days - Once per week - Twice per month - Once per month - Enable periodic backups - Backups output directory - Last successful backup: %s - x%.1f - Lock screen rotation - Manga - Hentai - Comics - Other - %1$s, %2$s - Sources catalog - Source enabled - There are no sources available in this section, or all of it might have been already added.\nStay tuned - No available manga sources found by your query - Catalog - Manage sources - Manual - Available: %1$d - Disable NSFW sources and hide adult manga from list if possible - Paused - Reduce memory consumption (beta) - Reduce offscreen pages quality to use less memory - State - Filtering by multiple genres is not supported by this manga source - Filtering by multiple states is not supported by this manga source - Search is not supported by this manga source - You can enable download slowdown for each manga source individually in the source settings if you are having problems with server-side blocking - Skip - Grayscale - Globally - This manga - These settings can be applied globally or only to the current manga. If applied globally, individual settings will not be overridden. - Apply - Filtering by both genres and locale is not supported by this source - Filtering by both genres and states is not supported by this source - Start typing the genre name - Might help with getting the download started if you have any issues with it - Please select which content sources you would like to enable. This can also be configured later in settings - Login to sync account - Restore - Backup date: %s - Upcoming - Name reversed - Content rating - Exclude genres - Safe - Suggestive - Adult - Default tab - Mark as completed - Mark selected manga as completely read?\n\nWarning: current reading progress will be lost. - This category was hidden from the main screen and is accessible through Menu → Manage categories - %1$s %2$s - Volume %d - Unknown volume - Your reading progress will not be saved - Vertical - Last read - Show menu - Show/hide UI - Previous chapter - Next chapter - Previous page - Next page - Reader actions - Configure actions for tappable screen areas - Enable volume buttons - Use volume buttons for switching pages - Tap action - Long tap action - None - Reset settings to default values? This action cannot be undone. - Use two pages layout on landscape orientation (beta) - Default webtoon zoom out - Fullscreen mode - Hide system status and navigation bars - %1$d/%2$d - Show estimated reading time - The time estimation value may be inaccurate - Suggestions feature is disabled - Checking for new chapters is disabled - Show labels in navigation bar - Saving pages - Ask for the destination dir every time - Default page save directory - Remove from history - Location - Preferred download format - Automatic - Single CBZ file - Multiple CBZ files - Reading statistics - Other manga - Less than a minute - Statistics - Clear statistics - Statistics cleared - Do you really want to clear all reading statistics? This action cannot be undone. - Week - Month - All time - Day - Three months - There are no statistics for the selected period - Pages read: %s - Alternatives - Migrate - Manga \"%1$s\" from \"%2$s\" will be replaced with \"%3$s\" from \"%4$s\" in your history and favorites (if present) - Manga migration - Migration completed - Delete read chapters - No chapters have been deleted - Removed %1$s, cleared %2$s - Delete chapters you have already read from local storage to free up space - This will permanently delete all chapters marked as read from your local storage. You can re-download it later, but the imported chapters may be lost forever - Delete read chapters automatically - Runs when the application starts - Split by translations - Show chapters with different translations separately, rather than in one list - Oldest - Long time ago read - Unread - Enable source - This manga source is not supported - Show pages thumbnails - Enable the \"Pages\" tab on the details screen - No data was received from server - Please select a proper Kotatsu backup file - (+%d) - + Cancel all + Download only via Wi-Fi + Stop downloading when switching to a mobile network + Suggestion: %s + Sometimes show notifications with suggested manga + More + Enable + No thanks + All active downloads will be cancelled, partially downloaded data will be lost + Your downloads history will be permanently deleted. No downloaded files will be affected + You don\'t have any downloads + Downloads have been resumed + Downloads have been paused + Downloads have been removed + Downloads have been cancelled + Do you want to receive personalized manga suggestions? + WebView not available: check if WebView provider is installed + Clear network cache + Type + Address + Port + Proxy + Invalid value + Kitsu + Enter your email and password to continue + Downloaded + Images optimization proxy + Use the wsrv.nl service to reduce traffic usage and speed up image loading if possible + Invert colors + Username + Password + Authorization (optional) + Invalid port number + Network + Data and privacy + Restore previously created backup + Allow zoom in gesture in webtoon mode + Show the current time and reading progress at the top of the screen + Show page numbers in bottom corner + Clear cookies for specified domain only. In most cases will invalidate authorization + All chapters with translation %s + The whole manga + First %s + Next unread %s + All unread chapters + All unread chapters (%s) + Select chapters manually + Pick custom directory + You have no access to this file or directory + Local manga directories + Description + This month + Voice search + Related manga + Light + Dark + White + Black + Background + Data was not restored + Make sure you have selected the correct backup file + Manage categories + Do not update suggestions using metered network connections + Do not check for new chapters using metered network connections + Enter manga title, genre or source name + Progress + Added + Show + %s requires a captcha to be resolved to work properly + Languages + Unknown + In progress + Disable NSFW + Too many requests. Try again later + Too many requests. Try again after %s + Show a list of related manga. In some cases it may be inaccurate or missing + Advanced + Manga list + Invalid data is returned or file is corrupted + On device + Directories + Main screen sections + No more items can be added + To top + Moved to top + Zoom out + Zoom in + Show zoom buttons + Whether to show zoom control buttons in the bottom right corner + Keep screen on + Do not turn the screen off while you\'re reading manga + Dropped + Reduces banding, but may impact performance + 32-bit color mode + Suggest new sources after app update + Prompt to enable newly added sources after updating the application + List options + Relevance + Categories + Online variant + Periodic backups + Backup creation frequency + Every day + Every 2 days + Once per week + Twice per month + Once per month + Enable periodic backups + Backups output directory + Last successful backup: %s + x%.1f + Lock screen rotation + Manga + Hentai + Comics + Other + %1$s, %2$s + Sources catalog + Source enabled + There are no sources available in this section, or all of it might have been already added.\nStay tuned + No available manga sources found by your query + Catalog + Manage sources + Manual + Available: %1$d + Disable NSFW sources and hide adult manga from list if possible + Paused + Reduce memory consumption (beta) + Reduce offscreen pages quality to use less memory + State + Filtering by multiple genres is not supported by this manga source + Filtering by multiple states is not supported by this manga source + Search is not supported by this manga source + You can enable download slowdown for each manga source individually in the source settings if you are having problems with server-side blocking + Skip + Grayscale + Globally + This manga + These settings can be applied globally or only to the current manga. If applied globally, individual settings will not be overridden. + Apply + Filtering by both genres and locale is not supported by this source + Filtering by both genres and states is not supported by this source + Start typing the genre name + Might help with getting the download started if you have any issues with it + Please select which content sources you would like to enable. This can also be configured later in settings + Login to sync account + Restore + Backup date: %s + Upcoming + Name reversed + Content rating + Exclude genres + Safe + Suggestive + Adult + Default tab + Mark as completed + Mark selected manga as completely read?\n\nWarning: current reading progress will be lost. + This category was hidden from the main screen and is accessible through Menu → Manage categories + %1$s %2$s + Volume %d + Unknown volume + Your reading progress will not be saved + Vertical + Last read + Show menu + Show/hide UI + Previous chapter + Next chapter + Previous page + Next page + Reader actions + Configure actions for tappable screen areas + Enable volume buttons + Use volume buttons for switching pages + Tap action + Long tap action + None + Reset settings to default values? This action cannot be undone. + Use two pages layout on landscape orientation (beta) + Default webtoon zoom out + Fullscreen mode + Hide system status and navigation bars + %1$d/%2$d + Show estimated reading time + The time estimation value may be inaccurate + Suggestions feature is disabled + Checking for new chapters is disabled + Show labels in navigation bar + Saving pages + Ask for the destination dir every time + Default page save directory + Remove from history + Location + Preferred download format + Automatic + Single CBZ file + Multiple CBZ files + Reading statistics + Other manga + Less than a minute + Statistics + Clear statistics + Statistics cleared + Do you really want to clear all reading statistics? This action cannot be undone. + Week + Month + All time + Day + Three months + There are no statistics for the selected period + Pages read: %s + Alternatives + Migrate + Manga \"%1$s\" from \"%2$s\" will be replaced with \"%3$s\" from \"%4$s\" in your history and favorites (if present) + Manga migration + Migration completed + Delete read chapters + No chapters have been deleted + Removed %1$s, cleared %2$s + Delete chapters you have already read from local storage to free up space + This will permanently delete all chapters marked as read from your local storage. You can re-download it later, but the imported chapters may be lost forever + Delete read chapters automatically + Runs when the application starts + Split by translations + Show chapters with different translations separately, rather than in one list + Oldest + Long time ago read + Unread + Enable source + This manga source is not supported + Show pages thumbnails + Enable the \"Pages\" tab on the details screen + No data was received from server + Please select a proper Kotatsu backup file + (+%d) + %d h - + %d m - + %d s - + %1$d h %2$d m - + %1$d m %2$d s - Fix - There is no permission to access manga on external storage - Last used - Show updated - Gaps in webtoon mode - Show vertical gaps between pages in webtoon mode - Less frequently - More frequently - Frequency of check - %1$s: %2$d - Pin navigation UI - Do not hide navigation bar and search view on scroll - Search suggestions - Recent queries - Suggested queries - Authors - You are blocked by the server. Try to use a different network connection (VPN, Proxy, etc.) - Disable - Sources disabled - Disable connectivity check - You can disable SSL certificates verification in case you face an SSL-related issues when accessing network resources. This may affect your security. Application restarting is required after changing this setting. - Skip the connectivity check in case you have issues with it (e.g. going offline mode even though the network is connected) - Disable NSFW notifications - Do not show notifications about NSFW manga updates - Checking for new chapters log - Debug information about background checks for new chapters - + Fix + There is no permission to access manga on external storage + Last used + Show updated + Gaps in webtoon mode + Show vertical gaps between pages in webtoon mode + Less frequently + More frequently + Frequency of check + %1$s: %2$d + Pin navigation UI + Do not hide navigation bar and search view on scroll + Search suggestions + Recent queries + Suggested queries + Authors + You are blocked by the server. Try to use a different network connection (VPN, Proxy, etc.) + Disable + Sources disabled + Disable connectivity check + You can disable SSL certificates verification in case you face an SSL-related issues when accessing network resources. This may affect your security. Application restarting is required after changing this setting. + Skip the connectivity check in case you have issues with it (e.g. going offline mode even though the network is connected) + Disable NSFW notifications + Do not show notifications about NSFW manga updates + Checking for new chapters log + Debug information about background checks for new chapters + New - All languages - Block when incognito mode - Preferred image server - %1$s: %2$s - Crop pages - Pin - Unpin - Source pinned - Source unpinned - Sources unpinned - Sources pinned - Recent sources - Percent read - Percent left - Chapters read - Chapters left - External/plugin - Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu - Plugin error: %s\n Make sure you are using the latest version of the plugin and Kotatsu - Connection is OK - Invalid proxy configuration - Show quick filters - Provides the ability to filter manga lists by certain parameters - SFW - Skip all - Stuck - Not in favoites - Updated long ago - Unpopular - Low rating - Ascending - Descending - Date - Popularity - Sign in to %s to continue - Sign in to set up integration with %s. This will allow you to track your manga reading progress and status - Unstable feature - This function is experimental. Please make sure you have a backup to avoid data loss - Background downloads - Download new chapters - Manga with downloaded chapters - Manga \"%1$s\" (%2$s) replaced with \"%3$s\" (%4$s) - Fixing manga - Fixed successfully - No fix required for \"%s\" - No alternatives found for \"%s\" - This function will find alternative sources for the selected manga. The task will take some time and will proceed in the background - Novel - Manhua - Manhwa - Recently added - Added long ago - Popular this hour - Popular today - Popular this week - Popular this month - Popular this year - Original language - Year - Demographics - Shounen - Shoujo - Seinen - Josei - Years - Any - This source does not support search with filters. Your filters have been cleared - Kodomo - One shot - Doujinshi - Image set - Artist CG - Game CG - Debug - Source code - User manual - Telegram group - Unsupported image format: %s - Invalid format: expected image but got %s - Start download - Save selected manga? This may consume traffic and disk space - Save manga - Genre - Download added - More options - Destination directory - You can select chapters to download by long click on item in the chapter list. - + All languages + Block when incognito mode + Preferred image server + %1$s: %2$s + Crop pages + Pin + Unpin + Source pinned + Source unpinned + Sources unpinned + Sources pinned + Recent sources + Percent read + Percent left + Chapters read + Chapters left + External/plugin + Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu + Plugin error: %s\n Make sure you are using the latest version of the plugin and Kotatsu + Connection is OK + Invalid proxy configuration + Show quick filters + Provides the ability to filter manga lists by certain parameters + SFW + Skip all + Stuck + Not in favoites + Updated long ago + Unpopular + Low rating + Ascending + Descending + Date + Popularity + Sign in to %s to continue + Sign in to set up integration with %s. This will allow you to track your manga reading progress and status + Unstable feature + This function is experimental. Please make sure you have a backup to avoid data loss + Background downloads + Download new chapters + Manga with downloaded chapters + Manga \"%1$s\" (%2$s) replaced with \"%3$s\" (%4$s) + Fixing manga + Fixed successfully + No fix required for \"%s\" + No alternatives found for \"%s\" + This function will find alternative sources for the selected manga. The task will take some time and will proceed in the background + Novel + Manhua + Manhwa + Recently added + Added long ago + Popular this hour + Popular today + Popular this week + Popular this month + Popular this year + Original language + Year + Demographics + Shounen + Shoujo + Seinen + Josei + Years + Any + This source does not support search with filters. Your filters have been cleared + Kodomo + One shot + Doujinshi + Image set + Artist CG + Game CG + Debug + Source code + User manual + Telegram group + Unsupported image format: %s + Invalid format: expected image but got %s + Start download + Save selected manga? This may consume traffic and disk space + Save manga + Genre + Download added + More options + Destination directory + You can select chapters to download by long click on item in the chapter list. + All - Downloading over cellular network - Allow downloads over cellular network? - Don\'t allow - Allow always - Allow once - Ask every time - Screen orientation - Portrait - Landscape - "]]> - Access denied (403) - Max number of backups - Delete old backups - Automatically delete old backup files to save storage space - Handle links - Handle manga links from external applications (e.g. web browser). You may also need to enable it manually in the application\'s system settings - Email - This source requires solving a captcha to continue - Author - Rating - Source - Translation - %1$s (%2$s) - Show slider - + Downloading over cellular network + Allow downloads over cellular network? + Don\'t allow + Allow always + Allow once + Ask every time + Screen orientation + Portrait + Landscape + "]]> + Access denied (403) + Max number of backups + Delete old backups + Automatically delete old backup files to save storage space + Handle links + Handle manga links from external applications (e.g. web browser). You may also need to enable it manually in the application\'s system settings + Email + This source requires solving a captcha to continue + Author + Rating + Source + Translation + %1$s (%2$s) + Show slider + Incognito Connection reset by remote host Check if API works @@ -793,4 +793,6 @@ All available manga sources will be enabled permanently All sources are enabled Transparent reader information bar + The backup will be restored in the background + Restoring backup diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bc8df7562..cc7e686d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ ksp = "2.0.21-1.0.28" leakcanary = "3.0-alpha-8" lifecycle = "2.8.7" markwon = "4.6.2" -material = "1.13.0-alpha09" +material = "1.13.0-alpha10" moshi = "1.15.2" okhttp = "4.12.0" okio = "3.10.2"