mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
Merge branch 'sync-part-final' into feat/add-sync-triggers-experimental
This commit is contained in:
commit
01f06ff1af
@ -26,10 +26,10 @@ class BasePreferences(
|
||||
|
||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
|
||||
enum class ExtensionInstaller(val titleRes: StringResource) {
|
||||
LEGACY(MR.strings.ext_installer_legacy),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku),
|
||||
PRIVATE(MR.strings.ext_installer_private),
|
||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -42,12 +43,15 @@ import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
@ -127,11 +131,24 @@ private fun ExtensionContent(
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||
item {
|
||||
WarningBanner(
|
||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||
modifier = Modifier.clickable {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
@ -384,6 +401,13 @@ private fun ExtensionItemActions(
|
||||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
@ -392,13 +416,6 @@ private fun ExtensionItemActions(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
|
@ -11,8 +11,6 @@ import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
@ -35,33 +33,29 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
internal class PermissionStep : OnboardingStep {
|
||||
|
||||
private var installGranted by mutableStateOf(false)
|
||||
private var notificationGranted by mutableStateOf(false)
|
||||
private var batteryGranted by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean
|
||||
get() = installGranted
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||
|
||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||
val observer = object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
installGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.packageManager.canRequestPackageInstalls()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Settings.Secure.getInt(context.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS) != 0
|
||||
}
|
||||
notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
@ -78,31 +72,16 @@ internal class PermissionStep : OnboardingStep {
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
) {
|
||||
SectionHeader(stringResource(MR.strings.onboarding_permission_type_required))
|
||||
|
||||
Column {
|
||||
PermissionItem(
|
||||
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
||||
granted = installGranted,
|
||||
onButtonClick = {
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
} else {
|
||||
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SectionHeader(stringResource(MR.strings.onboarding_permission_type_optional))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permissionRequester = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission(),
|
||||
|
@ -45,6 +45,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
||||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
@ -370,6 +371,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||
pref = extensionInstallerPref,
|
||||
title = stringResource(MR.strings.ext_installer_pref),
|
||||
entries = extensionInstallerPref.entries
|
||||
.filter {
|
||||
// TODO: allow private option in stable versions once URL handling is more fleshed out
|
||||
if (isPreviewBuildType || isDevFlavor) {
|
||||
true
|
||||
} else {
|
||||
it != BasePreferences.ExtensionInstaller.PRIVATE
|
||||
}
|
||||
}
|
||||
.associateWith { stringResource(it.titleRes) }
|
||||
.toImmutableMap(),
|
||||
onValueChanged = {
|
||||
|
@ -29,16 +29,11 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.PersistentSet
|
||||
import kotlinx.collections.immutable.minus
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.coroutines.flow.update
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||
@ -101,13 +96,13 @@ class CreateBackupScreen : Screen() {
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||
)
|
||||
}
|
||||
BackupChoices.forEach { (k, v) ->
|
||||
BackupOptions.entries.forEach { option ->
|
||||
item {
|
||||
LabeledCheckbox(
|
||||
label = stringResource(v),
|
||||
checked = state.flags.contains(k),
|
||||
label = stringResource(option.label),
|
||||
checked = option.getter(state.options),
|
||||
onCheckedChange = {
|
||||
model.toggleFlag(k)
|
||||
model.toggle(option.setter, it)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||
)
|
||||
@ -145,37 +140,20 @@ class CreateBackupScreen : Screen() {
|
||||
|
||||
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||
|
||||
fun toggleFlag(flag: Int) {
|
||||
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||
mutableState.update {
|
||||
if (it.flags.contains(flag)) {
|
||||
it.copy(flags = it.flags - flag)
|
||||
} else {
|
||||
it.copy(flags = it.flags + flag)
|
||||
}
|
||||
it.copy(
|
||||
options = setter(it.options, enabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createBackup(context: Context, uri: Uri) {
|
||||
val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b })
|
||||
BackupCreateJob.startNow(context, uri, flags)
|
||||
BackupCreateJob.startNow(context, uri, state.value.options)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val flags: PersistentSet<Int> = persistentSetOf(
|
||||
BackupCreateFlags.BACKUP_CATEGORY,
|
||||
BackupCreateFlags.BACKUP_CHAPTER,
|
||||
BackupCreateFlags.BACKUP_TRACK,
|
||||
BackupCreateFlags.BACKUP_HISTORY,
|
||||
),
|
||||
val options: BackupOptions = BackupOptions(),
|
||||
)
|
||||
}
|
||||
|
||||
private val BackupChoices = persistentMapOf(
|
||||
BackupCreateFlags.BACKUP_CATEGORY to MR.strings.categories,
|
||||
BackupCreateFlags.BACKUP_CHAPTER to MR.strings.chapters,
|
||||
BackupCreateFlags.BACKUP_TRACK to MR.strings.track,
|
||||
BackupCreateFlags.BACKUP_HISTORY to MR.strings.history,
|
||||
BackupCreateFlags.BACKUP_APP_PREFS to MR.strings.app_settings,
|
||||
BackupCreateFlags.BACKUP_SOURCE_PREFS to MR.strings.source_settings,
|
||||
)
|
||||
|
41
app/src/main/java/eu/kanade/presentation/util/Permissions.kt
Normal file
41
app/src/main/java/eu/kanade/presentation/util/Permissions.kt
Normal file
@ -0,0 +1,41 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
@Composable
|
||||
fun rememberRequestPackageInstallsPermissionState(): Boolean {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
var installGranted by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||
val observer = object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
installGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.packageManager.canRequestPackageInstalls()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Settings.Secure.getInt(context.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS) != 0
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
return installGranted
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.backup.create
|
||||
|
||||
internal object BackupCreateFlags {
|
||||
const val BACKUP_CATEGORY = 0x1
|
||||
const val BACKUP_CHAPTER = 0x2
|
||||
const val BACKUP_HISTORY = 0x4
|
||||
const val BACKUP_TRACK = 0x8
|
||||
const val BACKUP_APP_PREFS = 0x10
|
||||
const val BACKUP_SOURCE_PREFS = 0x20
|
||||
|
||||
const val AutomaticDefaults = BACKUP_CATEGORY or
|
||||
BACKUP_CHAPTER or
|
||||
BACKUP_HISTORY or
|
||||
BACKUP_TRACK or
|
||||
BACKUP_APP_PREFS or
|
||||
BACKUP_SOURCE_PREFS
|
||||
}
|
@ -19,6 +19,8 @@ import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.lang.asBooleanArray
|
||||
import eu.kanade.tachiyomi.util.lang.asDataClass
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
@ -47,10 +49,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
||||
val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
|
||||
?: BackupOptions()
|
||||
|
||||
return try {
|
||||
val location = BackupCreator(context, isAutoBackup).backup(uri, flags)
|
||||
val location = BackupCreator(context, isAutoBackup).backup(uri, options)
|
||||
if (!isAutoBackup) {
|
||||
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
||||
}
|
||||
@ -112,11 +115,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||
}
|
||||
}
|
||||
|
||||
fun startNow(context: Context, uri: Uri, flags: Int) {
|
||||
fun startNow(context: Context, uri: Uri, options: BackupOptions) {
|
||||
val inputData = workDataOf(
|
||||
IS_AUTO_BACKUP_KEY to false,
|
||||
LOCATION_URI_KEY to uri.toString(),
|
||||
BACKUP_FLAGS_KEY to flags,
|
||||
OPTIONS_KEY to options.asBooleanArray(),
|
||||
)
|
||||
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
|
||||
.addTag(TAG_MANUAL)
|
||||
@ -132,4 +135,4 @@ private const val TAG_MANUAL = "$TAG_AUTO:manual"
|
||||
|
||||
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
|
||||
private const val OPTIONS_KEY = "options" // BooleanArray
|
||||
|
@ -5,9 +5,6 @@ import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||
@ -52,7 +49,7 @@ class BackupCreator(
|
||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||
) {
|
||||
|
||||
suspend fun backup(uri: Uri, flags: Int): String {
|
||||
suspend fun backup(uri: Uri, options: BackupOptions): String {
|
||||
var file: UniFile? = null
|
||||
try {
|
||||
file = (
|
||||
@ -73,19 +70,18 @@ class BackupCreator(
|
||||
UniFile.fromUri(context, uri)
|
||||
}
|
||||
)
|
||||
?: throw Exception(context.stringResource(MR.strings.create_backup_file_error))
|
||||
|
||||
if (!file.isFile) {
|
||||
throw IllegalStateException("Failed to get handle on a backup file")
|
||||
if (file == null || !file.isFile) {
|
||||
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
|
||||
}
|
||||
|
||||
val databaseManga = getFavorites.await()
|
||||
val backup = Backup(
|
||||
backupManga = backupMangas(databaseManga, flags),
|
||||
backupCategories = backupCategories(flags),
|
||||
backupManga = backupMangas(databaseManga, options),
|
||||
backupCategories = backupCategories(options),
|
||||
backupSources = backupSources(databaseManga),
|
||||
backupPreferences = backupAppPreferences(flags),
|
||||
backupSourcePreferences = backupSourcePreferences(flags),
|
||||
backupPreferences = backupAppPreferences(options),
|
||||
backupSourcePreferences = backupSourcePreferences(options),
|
||||
)
|
||||
|
||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||
@ -118,30 +114,30 @@ class BackupCreator(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun backupCategories(options: Int): List<BackupCategory> {
|
||||
if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList()
|
||||
suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||
if (!options.categories) return emptyList()
|
||||
|
||||
return categoriesBackupCreator.backupCategories()
|
||||
}
|
||||
|
||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||
return mangaBackupCreator.backupMangas(mangas, flags)
|
||||
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||
return mangaBackupCreator.backupMangas(mangas, options)
|
||||
}
|
||||
|
||||
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||
return sourcesBackupCreator.backupSources(mangas)
|
||||
}
|
||||
|
||||
fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
||||
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
|
||||
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
if (!options.appSettings) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.backupAppPreferences()
|
||||
return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings)
|
||||
}
|
||||
|
||||
fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
||||
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
|
||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
if (!options.sourceSettings) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.backupSourcePreferences()
|
||||
return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.data.backup.create
|
||||
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
data class BackupOptions(
|
||||
val libraryEntries: Boolean = true,
|
||||
val categories: Boolean = true,
|
||||
val chapters: Boolean = true,
|
||||
val tracking: Boolean = true,
|
||||
val history: Boolean = true,
|
||||
val appSettings: Boolean = true,
|
||||
val sourceSettings: Boolean = true,
|
||||
val privateSettings: Boolean = false,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val entries = persistentListOf(
|
||||
Entry(
|
||||
label = MR.strings.categories,
|
||||
getter = BackupOptions::categories,
|
||||
setter = { options, enabled -> options.copy(categories = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.chapters,
|
||||
getter = BackupOptions::chapters,
|
||||
setter = { options, enabled -> options.copy(chapters = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.track,
|
||||
getter = BackupOptions::tracking,
|
||||
setter = { options, enabled -> options.copy(tracking = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.history,
|
||||
getter = BackupOptions::history,
|
||||
setter = { options, enabled -> options.copy(history = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.app_settings,
|
||||
getter = BackupOptions::appSettings,
|
||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.source_settings,
|
||||
getter = BackupOptions::sourceSettings,
|
||||
setter = { options, enabled -> options.copy(sourceSettings = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.private_settings,
|
||||
getter = BackupOptions::privateSettings,
|
||||
setter = { options, enabled -> options.copy(privateSettings = enabled) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class Entry(
|
||||
val label: StringResource,
|
||||
val getter: (BackupOptions) -> Boolean,
|
||||
val setter: (BackupOptions, Boolean) -> BackupOptions,
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
@ -20,18 +20,17 @@ class MangaBackupCreator(
|
||||
private val getHistory: GetHistory = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||
return mangas.map {
|
||||
backupManga(it, flags)
|
||||
backupManga(it, options)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
||||
private suspend fun backupManga(manga: Manga, options: BackupOptions): BackupManga {
|
||||
// Entry for this manga
|
||||
val mangaObject = manga.toBackupManga()
|
||||
|
||||
// Check if user wants chapter information in backup
|
||||
if (options and BackupCreateFlags.BACKUP_CHAPTER == BackupCreateFlags.BACKUP_CHAPTER) {
|
||||
if (options.chapters) {
|
||||
// Backup all the chapters
|
||||
handler.awaitList {
|
||||
chaptersQueries.getChaptersByMangaId(
|
||||
@ -44,8 +43,7 @@ class MangaBackupCreator(
|
||||
?.let { mangaObject.chapters = it }
|
||||
}
|
||||
|
||||
// Check if user wants category information in backup
|
||||
if (options and BackupCreateFlags.BACKUP_CATEGORY == BackupCreateFlags.BACKUP_CATEGORY) {
|
||||
if (options.categories) {
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = getCategories.await(manga.id)
|
||||
if (categoriesForManga.isNotEmpty()) {
|
||||
@ -53,16 +51,14 @@ class MangaBackupCreator(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants track information in backup
|
||||
if (options and BackupCreateFlags.BACKUP_TRACK == BackupCreateFlags.BACKUP_TRACK) {
|
||||
if (options.tracking) {
|
||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
||||
if (tracks.isNotEmpty()) {
|
||||
mangaObject.tracking = tracks
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BackupCreateFlags.BACKUP_HISTORY == BackupCreateFlags.BACKUP_HISTORY) {
|
||||
if (options.history) {
|
||||
val historyByMangaId = getHistory.await(manga.id)
|
||||
if (historyByMangaId.isNotEmpty()) {
|
||||
val history = historyByMangaId.map { history ->
|
||||
|
@ -22,26 +22,27 @@ class PreferenceBackupCreator(
|
||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||
) {
|
||||
|
||||
fun backupAppPreferences(): List<BackupPreference> {
|
||||
fun backupAppPreferences(includePrivatePreferences: Boolean): List<BackupPreference> {
|
||||
return preferenceStore.getAll().toBackupPreferences()
|
||||
.withPrivatePreferences(includePrivatePreferences)
|
||||
}
|
||||
|
||||
fun backupSourcePreferences(): List<BackupSourcePreferences> {
|
||||
fun backupSourcePreferences(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filterIsInstance<ConfigurableSource>()
|
||||
.map {
|
||||
BackupSourcePreferences(
|
||||
it.preferenceKey(),
|
||||
it.sourcePreferences().all.toBackupPreferences(),
|
||||
it.sourcePreferences().all.toBackupPreferences()
|
||||
.withPrivatePreferences(includePrivatePreferences),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
||||
return this.filterKeys {
|
||||
!Preference.isPrivate(it) && !Preference.isAppState(it)
|
||||
}
|
||||
return this
|
||||
.filterKeys { !Preference.isAppState(it) }
|
||||
.mapNotNull { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
||||
@ -56,4 +57,11 @@ class PreferenceBackupCreator(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<BackupPreference>.withPrivatePreferences(include: Boolean) =
|
||||
if (include) {
|
||||
this
|
||||
} else {
|
||||
this.filter { !Preference.isPrivate(it.key) }
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.lang.asBooleanArray
|
||||
import eu.kanade.tachiyomi.util.lang.asDataClass
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
@ -30,8 +32,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||
val options = inputData.getBooleanArray(OPTIONS_KEY)
|
||||
?.let { RestoreOptions.fromBooleanArray(it) }
|
||||
val options: RestoreOptions? = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
|
||||
|
||||
if (uri == null || options == null) {
|
||||
return Result.failure()
|
||||
@ -84,7 +85,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
||||
val inputData = workDataOf(
|
||||
LOCATION_URI_KEY to uri.toString(),
|
||||
SYNC_KEY to sync,
|
||||
OPTIONS_KEY to options.toBooleanArray(),
|
||||
OPTIONS_KEY to options.asBooleanArray(),
|
||||
)
|
||||
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
||||
.addTag(TAG)
|
||||
|
@ -4,14 +4,4 @@ data class RestoreOptions(
|
||||
val appSettings: Boolean = true,
|
||||
val sourceSettings: Boolean = true,
|
||||
val library: Boolean = true,
|
||||
) {
|
||||
fun toBooleanArray() = booleanArrayOf(appSettings, sourceSettings, library)
|
||||
|
||||
companion object {
|
||||
fun fromBooleanArray(booleanArray: BooleanArray) = RestoreOptions(
|
||||
appSettings = booleanArray[0],
|
||||
sourceSettings = booleanArray[1],
|
||||
library = booleanArray[2],
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
@ -72,12 +72,22 @@ class SyncManager(
|
||||
*/
|
||||
suspend fun syncData() {
|
||||
val databaseManga = getAllMangaFromDB()
|
||||
val backupOptions = BackupOptions(
|
||||
libraryEntries = true,
|
||||
categories = true,
|
||||
chapters = true,
|
||||
tracking = true,
|
||||
history = true,
|
||||
appSettings = true,
|
||||
sourceSettings = true,
|
||||
privateSettings = true,
|
||||
)
|
||||
val backup = Backup(
|
||||
backupManga = backupCreator.backupMangas(databaseManga, BackupCreateFlags.AutomaticDefaults),
|
||||
backupCategories = backupCreator.backupCategories(BackupCreateFlags.AutomaticDefaults),
|
||||
backupManga = backupCreator.backupMangas(databaseManga, backupOptions),
|
||||
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||
backupSources = backupCreator.backupSources(databaseManga),
|
||||
backupPreferences = backupCreator.backupAppPreferences(BackupCreateFlags.AutomaticDefaults),
|
||||
backupSourcePreferences = backupCreator.backupSourcePreferences(BackupCreateFlags.AutomaticDefaults),
|
||||
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
||||
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
||||
)
|
||||
|
||||
// Create the SyncData object
|
||||
|
@ -120,7 +120,7 @@ abstract class SyncService(
|
||||
remote.copy(chapters = mergedChapters)
|
||||
}
|
||||
}
|
||||
else -> null // This case occurs if both are null, which shouldn't happen but is handled for completeness.
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -304,7 +304,7 @@ internal object ExtensionLoader {
|
||||
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
|
||||
is Source -> listOf(obj)
|
||||
is SourceFactory -> obj.createSources()
|
||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||
else -> throw Exception("Unknown source class type: ${obj.javaClass}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
|
||||
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
|
||||
@ -34,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class ExtensionsScreenModel(
|
||||
preferences: SourcePreferences = Injekt.get(),
|
||||
basePreferences: BasePreferences = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
||||
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
|
||||
@ -124,6 +126,10 @@ class ExtensionsScreenModel(
|
||||
preferences.extensionUpdatesCount().changes()
|
||||
.onEach { mutableState.update { state -> state.copy(updates = it) } }
|
||||
.launchIn(screenModelScope)
|
||||
|
||||
basePreferences.extensionInstaller().changes()
|
||||
.onEach { mutableState.update { state -> state.copy(installer = it) } }
|
||||
.launchIn(screenModelScope)
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
@ -199,6 +205,7 @@ class ExtensionsScreenModel(
|
||||
val isRefreshing: Boolean = false,
|
||||
val items: ItemGroups = mutableMapOf(),
|
||||
val updates: Int = 0,
|
||||
val installer: BasePreferences.ExtensionInstaller? = null,
|
||||
val searchQuery: String? = null,
|
||||
) {
|
||||
val isEmpty = items.isEmpty()
|
||||
|
@ -0,0 +1,18 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
|
||||
fun <T : Any> T.asBooleanArray(): BooleanArray {
|
||||
return this::class.declaredMemberProperties
|
||||
.filterIsInstance<KProperty1<T, Boolean>>()
|
||||
.map { it.get(this) }
|
||||
.toBooleanArray()
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> BooleanArray.asDataClass(): T {
|
||||
val properties = T::class.declaredMemberProperties.filterIsInstance<KProperty1<T, Boolean>>()
|
||||
require(properties.size == this.size) { "Boolean array size does not match data class property count" }
|
||||
return T::class.primaryConstructor!!.call(*this.toTypedArray())
|
||||
}
|
@ -9,6 +9,7 @@ import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
@ -167,3 +168,14 @@ fun Context.isInstalledFromFDroid(): Boolean {
|
||||
// F-Droid builds typically disable the updater
|
||||
(!BuildConfig.INCLUDE_UPDATER && !isDevFlavor)
|
||||
}
|
||||
|
||||
fun Context.launchRequestPackageInstallsPermission() {
|
||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
} else {
|
||||
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ WHERE _id IN :chapterIds;
|
||||
|
||||
insert:
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, strftime('%s', 'now'));
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0);
|
||||
|
||||
update:
|
||||
UPDATE chapters
|
||||
|
@ -117,7 +117,7 @@ AND source IN :sourceIds;
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at)
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, strftime('%s', 'now'));
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0);
|
||||
|
||||
update:
|
||||
UPDATE mangas SET
|
||||
|
@ -20,7 +20,7 @@ END;
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas_categories(manga_id, category_id, last_modified_at)
|
||||
VALUES (:mangaId, :categoryId, strftime('%s', 'now'));
|
||||
VALUES (:mangaId, :categoryId, 0);
|
||||
|
||||
deleteMangaCategoryByMangaId:
|
||||
DELETE FROM mangas_categories
|
||||
|
@ -184,8 +184,6 @@
|
||||
<string name="onboarding_storage_info">Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s</string>
|
||||
<string name="onboarding_storage_action_select">Select a folder</string>
|
||||
<string name="onboarding_storage_selection_required">A folder must be selected</string>
|
||||
<string name="onboarding_permission_type_required">Required</string>
|
||||
<string name="onboarding_permission_type_optional">Optional</string>
|
||||
<string name="onboarding_permission_install_apps">Install apps permission</string>
|
||||
<string name="onboarding_permission_install_apps_description">To install source extensions.</string>
|
||||
<string name="onboarding_permission_notifications">Notification permission</string>
|
||||
@ -334,6 +332,7 @@
|
||||
<string name="ext_info_age_rating">Age rating</string>
|
||||
<string name="ext_nsfw_short">18+</string>
|
||||
<string name="ext_nsfw_warning">Sources from this extension may contain NSFW (18+) content</string>
|
||||
<string name="ext_permission_install_apps_warning">Permissions are needed to install extensions. Tap here to grant.</string>
|
||||
<string name="ext_install_service_notif">Installing extension…</string>
|
||||
<string name="ext_installer_pref">Installer</string>
|
||||
<string name="ext_installer_legacy">Legacy</string>
|
||||
@ -510,6 +509,7 @@
|
||||
<string name="backup_choice">What do you want to backup?</string>
|
||||
<string name="app_settings">App settings</string>
|
||||
<string name="source_settings">Source settings</string>
|
||||
<string name="private_settings">Include sensitive settings (e.g., tracker login tokens)</string>
|
||||
<string name="creating_backup">Creating backup</string>
|
||||
<string name="creating_backup_error">Backup failed</string>
|
||||
<string name="missing_storage_permission">Storage permissions not granted</string>
|
||||
|
@ -473,4 +473,4 @@ abstract class HttpSource : CatalogueSource {
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
||||
|
||||
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")
|
||||
class LicensedMangaChaptersException : RuntimeException()
|
||||
|
Loading…
Reference in New Issue
Block a user