diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt index 95a80d272..673b8ee1e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/domain/AlternativesUseCase.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.alternatives.domain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -40,13 +39,16 @@ class AlternativesUseCase @Inject constructor( searchHelper(manga.title, SearchKind.TITLE)?.manga } }.getOrNull() - list?.forEach { send(it) } + list?.forEach { + launch { + val details = runCatchingCancellable { + mangaRepositoryFactory.create(it.source).getDetails(it) + }.getOrDefault(it) + send(details) + } + } } } - }.map { - runCatchingCancellable { - mangaRepositoryFactory.create(it.source).getDetails(it) - }.getOrDefault(it) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupEntry.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupEntry.kt index ae92bff4e..9dd16bc43 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupEntry.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupEntry.kt @@ -16,6 +16,7 @@ class BackupEntry( CATEGORIES("categories"), FAVOURITES("favourites"), SETTINGS("settings"), + SETTINGS_READER_GRID("reader_grid"), BOOKMARKS("bookmarks"), SOURCES("sources"), } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 3c3d33ffc..5a63fd5aa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.parsers.util.json.asTypedList import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.data.TapGridSettings import java.util.Date import javax.inject.Inject @@ -20,6 +21,7 @@ private const val PAGE_SIZE = 10 class BackupRepository @Inject constructor( private val db: MangaDatabase, private val settings: AppSettings, + private val tapGridSettings: TapGridSettings, ) { suspend fun dumpHistory(): BackupEntry { @@ -105,6 +107,14 @@ class BackupRepository @Inject constructor( return entry } + fun dumpReaderGridSettings(): BackupEntry { + val entry = BackupEntry(BackupEntry.Name.SETTINGS_READER_GRID, JSONArray()) + val settingsDump = tapGridSettings.getAllValues() + val json = JsonSerializer(settingsDump).toJson() + entry.data.put(json) + return entry + } + suspend fun dumpSources(): BackupEntry { val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray()) val all = db.getSourcesDao().findAll() @@ -229,4 +239,14 @@ class BackupRepository @Inject constructor( } return result } + + fun restoreReaderGridSettings(entry: BackupEntry): CompositeResult { + val result = CompositeResult() + for (item in entry.data.asTypedList()) { + result += runCatchingCancellable { + tapGridSettings.upsertAll(JsonDeserializer(item).toMap()) + } + } + return result + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 4dec165f5..d1f88b863 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -15,13 +15,13 @@ import androidx.core.os.LocaleListCompat import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext -import org.json.JSONArray import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.getEnumValue import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.putAll import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.toUriOrNull @@ -569,20 +569,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { fun getAllValues(): Map = prefs.all - fun upsertAll(m: Map) { - prefs.edit { - m.forEach { e -> - when (val v = e.value) { - is Boolean -> putBoolean(e.key, v) - is Int -> putInt(e.key, v) - is Long -> putLong(e.key, v) - is Float -> putFloat(e.key, v) - is String -> putString(e.key, v) - is JSONArray -> putStringSet(e.key, v.toStringSet()) - } - } - } - } + fun upsertAll(m: Map) = prefs.edit { putAll(m) } private fun isBackgroundNetworkRestricted(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -592,15 +579,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { } } - private fun JSONArray.toStringSet(): Set { - val len = length() - val result = ArraySet(len) - for (i in 0 until len) { - result.add(getString(i)) - } - return result - } - companion object { const val TRACK_HISTORY = "history" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt index 049f64a87..44a2475bd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt @@ -13,19 +13,16 @@ import android.content.Context.POWER_SERVICE import android.content.ContextWrapper import android.content.Intent import android.content.OperationApplicationException -import android.content.SharedPreferences import android.content.SyncResult import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.pm.ResolveInfo import android.database.SQLException import android.graphics.Bitmap -import android.graphics.Color import android.net.ConnectivityManager import android.os.Build import android.os.PowerManager import android.provider.Settings import android.view.ViewPropertyAnimator -import android.view.Window import android.webkit.CookieManager import android.webkit.WebView import androidx.activity.result.ActivityResultLauncher @@ -37,7 +34,6 @@ import androidx.appcompat.app.AppCompatDialog import androidx.core.app.ActivityOptionsCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import androidx.core.graphics.ColorUtils import androidx.core.os.LocaleListCompat import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment @@ -46,15 +42,8 @@ import androidx.lifecycle.coroutineScope import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import androidx.work.CoroutineWorker -import com.google.android.material.elevation.ElevationOverlayProvider import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible import okio.IOException @@ -68,7 +57,6 @@ import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.File import kotlin.math.roundToLong -import com.google.android.material.R as materialR val Context.activityManager: ActivityManager? get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager @@ -101,25 +89,6 @@ fun ActivityResultLauncher.tryLaunch( e.printStackTraceDebug() }.isSuccess -fun SharedPreferences.observe(): Flow = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - trySendBlocking(key) - } - registerOnSharedPreferenceChangeListener(listener) - awaitClose { - unregisterOnSharedPreferenceChangeListener(listener) - } -} - -fun SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow = flow { - emit(valueProducer()) - observe().collect { upstreamKey -> - if (upstreamKey == key) { - emit(valueProducer()) - } - } -}.distinctUntilChanged() - fun Lifecycle.postDelayed(delay: Long, runnable: Runnable) { coroutineScope.launch { delay(delay) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt index 611d4db01..b48ca8488 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Preferences.kt @@ -1,8 +1,16 @@ package org.koitharu.kotatsu.core.util.ext import android.content.SharedPreferences +import androidx.collection.ArraySet import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import org.json.JSONArray fun ListPreference.setDefaultValueCompat(defaultValue: String) { if (value == null) { @@ -28,3 +36,44 @@ fun > SharedPreferences.getEnumValue(key: String, defaultValue: E): fun > SharedPreferences.Editor.putEnumValue(key: String, value: E?) { putString(key, value?.name) } + +fun SharedPreferences.observe(): Flow = callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + trySendBlocking(key) + } + registerOnSharedPreferenceChangeListener(listener) + awaitClose { + unregisterOnSharedPreferenceChangeListener(listener) + } +} + +fun SharedPreferences.observe(key: String, valueProducer: suspend () -> T): Flow = flow { + emit(valueProducer()) + observe().collect { upstreamKey -> + if (upstreamKey == key) { + emit(valueProducer()) + } + } +}.distinctUntilChanged() + +fun SharedPreferences.Editor.putAll(values: Map) { + values.forEach { e -> + when (val v = e.value) { + is Boolean -> putBoolean(e.key, v) + is Int -> putInt(e.key, v) + is Long -> putLong(e.key, v) + is Float -> putFloat(e.key, v) + is String -> putString(e.key, v) + is JSONArray -> putStringSet(e.key, v.toStringSet()) + } + } +} + +private fun JSONArray.toStringSet(): Set { + val len = length() + val result = ArraySet(len) + for (i in 0 until len) { + result.add(getString(i)) + } + return result +} 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 index 503c76e73..8fa0dc62e 100644 --- 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 @@ -14,6 +14,9 @@ data class Progress( val isFull: Boolean get() = progress == total + val isIndeterminate: Boolean + get() = total < 0 + override fun compareTo(other: Progress): Int = if (total == other.total) { progress.compareTo(other.progress) } else { @@ -44,4 +47,9 @@ data class Progress( ) fun percentSting() = (percent * 100f).toInt().toString() + + companion object { + + val INDETERMINATE = Progress(0, -1) + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt index d3fc3dab1..aaa555259 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt @@ -3,16 +3,19 @@ package org.koitharu.kotatsu.reader.data import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import dagger.Reusable import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import org.koitharu.kotatsu.core.util.ext.getEnumValue import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.putAll import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.reader.domain.TapGridArea import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction import javax.inject.Inject +@Reusable class TapGridSettings @Inject constructor(@ApplicationContext context: Context) { private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -43,6 +46,10 @@ class TapGridSettings @Inject constructor(@ApplicationContext context: Context) fun observe() = prefs.observe().flowOn(Dispatchers.IO) + fun getAllValues(): Map = prefs.all + + fun upsertAll(m: Map) = prefs.edit { putAll(m) } + private fun initPrefs(withDefaultValues: Boolean) { prefs.edit { clear() 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 cef00b6f4..e906b6f31 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 @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.reader.data.TapGridSettings import java.io.File import java.io.FileDescriptor import java.io.FileInputStream @@ -38,7 +39,14 @@ class AppBackupAgent : BackupAgent() { override fun onFullBackup(data: FullBackupDataOutput) { super.onFullBackup(data) val file = - createBackupFile(this, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext))) + createBackupFile( + this, + BackupRepository( + MangaDatabase(context = applicationContext), + AppSettings(applicationContext), + TapGridSettings(applicationContext), + ), + ) try { fullBackupFile(file, data) } finally { @@ -58,7 +66,11 @@ class AppBackupAgent : BackupAgent() { restoreBackupFile( data.fileDescriptor, size, - BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)), + BackupRepository( + db = MangaDatabase(applicationContext), + settings = AppSettings(applicationContext), + tapGridSettings = TapGridSettings(applicationContext), + ), ) destination.delete() } else { @@ -76,6 +88,7 @@ class AppBackupAgent : BackupAgent() { backup.put(repository.dumpBookmarks()) backup.put(repository.dumpSources()) backup.put(repository.dumpSettings()) + backup.put(repository.dumpReaderGridSettings()) backup.finish() backup.file } @@ -103,6 +116,7 @@ class AppBackupAgent : BackupAgent() { 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) } + backup.getEntry(BackupEntry.Name.SETTINGS_READER_GRID)?.let { repository.restoreReaderGridSettings(it) } } } finally { backup.close() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index 1bae292a9..0cdaac346 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -16,10 +16,10 @@ import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.tryLaunch +import org.koitharu.kotatsu.core.util.progress.Progress import org.koitharu.kotatsu.databinding.DialogProgressBinding import java.io.File import java.io.FileOutputStream -import kotlin.math.roundToInt @AndroidEntryPoint class BackupDialogFragment : AlertDialogFragment() { @@ -68,13 +68,14 @@ class BackupDialogFragment : AlertDialogFragment() { dismiss() } - private fun onProgressChanged(value: Float) { + private fun onProgressChanged(value: Progress) { with(requireViewBinding().progressBar) { isVisible = true val wasIndeterminate = isIndeterminate - isIndeterminate = value < 0 - if (value >= 0) { - setProgressCompat((value * max).roundToInt(), !wasIndeterminate) + isIndeterminate = value.isIndeterminate + if (!value.isIndeterminate) { + max = value.total + setProgressCompat(value.progress, !wasIndeterminate) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupEntryModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupEntryModel.kt index 632807dec..aede79ca3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupEntryModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupEntryModel.kt @@ -20,6 +20,7 @@ data class BackupEntryModel( BackupEntry.Name.CATEGORIES -> R.string.favourites_categories BackupEntry.Name.FAVOURITES -> R.string.favourites BackupEntry.Name.SETTINGS -> R.string.settings + BackupEntry.Name.SETTINGS_READER_GRID -> R.string.reader_actions BackupEntry.Name.BOOKMARKS -> R.string.bookmarks BackupEntry.Name.SOURCES -> R.string.remote_sources } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt index 62e21aa00..f31e8dbc2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.backup.BackupZipOutput 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.progress.Progress import java.io.File import javax.inject.Inject @@ -18,35 +19,37 @@ class BackupViewModel @Inject constructor( @ApplicationContext context: Context, ) : BaseViewModel() { - val progress = MutableStateFlow(-1f) + val progress = MutableStateFlow(Progress.INDETERMINATE) val onBackupDone = MutableEventFlow() init { launchLoadingJob { val file = BackupZipOutput.createTemp(context).use { backup -> - val step = 1f / 6f + progress.value = Progress(0, 7) backup.put(repository.createIndex()) - progress.value = 0f backup.put(repository.dumpHistory()) + progress.value = progress.value.inc() - progress.value += step backup.put(repository.dumpCategories()) + progress.value = progress.value.inc() - progress.value += step backup.put(repository.dumpFavourites()) + progress.value = progress.value.inc() - progress.value += step backup.put(repository.dumpBookmarks()) + progress.value = progress.value.inc() - progress.value += step backup.put(repository.dumpSources()) + progress.value = progress.value.inc() - progress.value += step backup.put(repository.dumpSettings()) + progress.value = progress.value.inc() + + backup.put(repository.dumpReaderGridSettings()) + progress.value = progress.value.inc() backup.finish() - progress.value = 1f backup.file } onBackupDone.call(file) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupService.kt index 8b7959fdd..06f882881 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupService.kt @@ -43,6 +43,7 @@ class PeriodicalBackupService : CoroutineIntentService() { backup.put(repository.dumpBookmarks()) backup.put(repository.dumpSources()) backup.put(repository.dumpSettings()) + backup.put(repository.dumpReaderGridSettings()) backup.finish() } externalBackupStorage.put(output.file) 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 index 699690afb..ca194b6d3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt @@ -164,6 +164,15 @@ class RestoreService : CoroutineIntentService() { notify() + if (BackupEntry.Name.SETTINGS_READER_GRID in entries) { + input.getEntry(BackupEntry.Name.SETTINGS_READER_GRID)?.let { + result += repository.restoreReaderGridSettings(it) + } + progress++ + } + + notify() + return result } diff --git a/app/src/main/res/layout/sheet_favorite_categories.xml b/app/src/main/res/layout/sheet_favorite_categories.xml index c048e6a81..246fd6148 100644 --- a/app/src/main/res/layout/sheet_favorite_categories.xml +++ b/app/src/main/res/layout/sheet_favorite_categories.xml @@ -82,6 +82,7 @@ android:layout_marginEnd="@dimen/margin_normal" android:ellipsize="end" android:gravity="center_vertical|start" + android:maxLines="3" android:textAppearance="?attr/textAppearanceBodyLarge" app:layout_constraintBottom_toBottomOf="@id/guideline" app:layout_constraintEnd_toEndOf="parent"