mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 17:09:19 +01:00
chore: merge upstream.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
commit
cf58a6d871
@ -26,10 +26,10 @@ class BasePreferences(
|
|||||||
|
|
||||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||||
|
|
||||||
enum class ExtensionInstaller(val titleRes: StringResource) {
|
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||||
LEGACY(MR.strings.ext_installer_legacy),
|
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller),
|
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||||
SHIZUKU(MR.strings.ext_installer_shizuku),
|
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||||
PRIVATE(MR.strings.ext_installer_private),
|
PRIVATE(MR.strings.ext_installer_private, false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.browse
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -42,12 +43,15 @@ import androidx.compose.ui.unit.dp
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
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.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
@ -127,11 +131,24 @@ private fun ExtensionContent(
|
|||||||
onOpenExtension: (Extension.Installed) -> Unit,
|
onOpenExtension: (Extension.Installed) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||||
|
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding + topSmallPaddingValues,
|
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) ->
|
state.items.forEach { (header, items) ->
|
||||||
item(
|
item(
|
||||||
contentType = "header",
|
contentType = "header",
|
||||||
@ -384,6 +401,13 @@ private fun ExtensionItemActions(
|
|||||||
installStep == InstallStep.Idle -> {
|
installStep == InstallStep.Idle -> {
|
||||||
when (extension) {
|
when (extension) {
|
||||||
is Extension.Installed -> {
|
is Extension.Installed -> {
|
||||||
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = stringResource(MR.strings.action_settings),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
if (extension.hasUpdate) {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
Icon(
|
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 -> {
|
is Extension.Untrusted -> {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
|
@ -11,8 +11,6 @@ import android.provider.Settings
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Column
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
@ -35,33 +33,29 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
internal class PermissionStep : OnboardingStep {
|
internal class PermissionStep : OnboardingStep {
|
||||||
|
|
||||||
private var installGranted by mutableStateOf(false)
|
|
||||||
private var notificationGranted by mutableStateOf(false)
|
private var notificationGranted by mutableStateOf(false)
|
||||||
private var batteryGranted by mutableStateOf(false)
|
private var batteryGranted by mutableStateOf(false)
|
||||||
|
|
||||||
override val isComplete: Boolean
|
override val isComplete: Boolean = true
|
||||||
get() = installGranted
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
val installGranted = rememberRequestPackageInstallsPermissionState()
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner.lifecycle) {
|
DisposableEffect(lifecycleOwner.lifecycle) {
|
||||||
val observer = object : DefaultLifecycleObserver {
|
val observer = object : DefaultLifecycleObserver {
|
||||||
override fun onResume(owner: LifecycleOwner) {
|
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) {
|
notificationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
||||||
PackageManager.PERMISSION_GRANTED
|
PackageManager.PERMISSION_GRANTED
|
||||||
@ -78,31 +72,16 @@ internal class PermissionStep : OnboardingStep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column {
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
|
||||||
) {
|
|
||||||
SectionHeader(stringResource(MR.strings.onboarding_permission_type_required))
|
|
||||||
|
|
||||||
PermissionItem(
|
PermissionItem(
|
||||||
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
title = stringResource(MR.strings.onboarding_permission_install_apps),
|
||||||
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
|
||||||
granted = installGranted,
|
granted = installGranted,
|
||||||
onButtonClick = {
|
onButtonClick = {
|
||||||
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
context.launchRequestPackageInstallsPermission()
|
||||||
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
|
||||||
data = Uri.parse("package:${context.packageName}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
SectionHeader(stringResource(MR.strings.onboarding_permission_type_optional))
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val permissionRequester = rememberLauncherForActivityResult(
|
val permissionRequester = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission(),
|
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.network.PREF_DOH_SHECAN
|
||||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
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.isPreviewBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||||
@ -370,6 +371,14 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
pref = extensionInstallerPref,
|
pref = extensionInstallerPref,
|
||||||
title = stringResource(MR.strings.ext_installer_pref),
|
title = stringResource(MR.strings.ext_installer_pref),
|
||||||
entries = extensionInstallerPref.entries
|
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) }
|
.associateWith { stringResource(it.titleRes) }
|
||||||
.toImmutableMap(),
|
.toImmutableMap(),
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
|
@ -29,16 +29,11 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.util.Screen
|
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.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
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.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
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 kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.LabeledCheckbox
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
@ -101,13 +96,13 @@ class CreateBackupScreen : Screen() {
|
|||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BackupChoices.forEach { (k, v) ->
|
BackupOptions.entries.forEach { option ->
|
||||||
item {
|
item {
|
||||||
LabeledCheckbox(
|
LabeledCheckbox(
|
||||||
label = stringResource(v),
|
label = stringResource(option.label),
|
||||||
checked = state.flags.contains(k),
|
checked = option.getter(state.options),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
model.toggleFlag(k)
|
model.toggle(option.setter, it)
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
)
|
)
|
||||||
@ -145,37 +140,20 @@ class CreateBackupScreen : Screen() {
|
|||||||
|
|
||||||
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel.State>(State()) {
|
||||||
|
|
||||||
fun toggleFlag(flag: Int) {
|
fun toggle(setter: (BackupOptions, Boolean) -> BackupOptions, enabled: Boolean) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
if (it.flags.contains(flag)) {
|
it.copy(
|
||||||
it.copy(flags = it.flags - flag)
|
options = setter(it.options, enabled),
|
||||||
} else {
|
)
|
||||||
it.copy(flags = it.flags + flag)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBackup(context: Context, uri: Uri) {
|
fun createBackup(context: Context, uri: Uri) {
|
||||||
val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b })
|
BackupCreateJob.startNow(context, uri, state.value.options)
|
||||||
BackupCreateJob.startNow(context, uri, flags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val flags: PersistentSet<Int> = persistentSetOf(
|
val options: BackupOptions = BackupOptions(),
|
||||||
BackupCreateFlags.BACKUP_CATEGORY,
|
|
||||||
BackupCreateFlags.BACKUP_CHAPTER,
|
|
||||||
BackupCreateFlags.BACKUP_TRACK,
|
|
||||||
BackupCreateFlags.BACKUP_HISTORY,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
@ -47,10 +49,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
|
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
|
||||||
|
?: BackupOptions()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val location = BackupCreator(context, isAutoBackup).backup(uri, flags)
|
val location = BackupCreator(context, isAutoBackup).backup(uri, options)
|
||||||
if (!isAutoBackup) {
|
if (!isAutoBackup) {
|
||||||
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
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(
|
val inputData = workDataOf(
|
||||||
IS_AUTO_BACKUP_KEY to false,
|
IS_AUTO_BACKUP_KEY to false,
|
||||||
LOCATION_URI_KEY to uri.toString(),
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
BACKUP_FLAGS_KEY to flags,
|
OPTIONS_KEY to options.asBooleanArray(),
|
||||||
)
|
)
|
||||||
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
|
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
|
||||||
.addTag(TAG_MANUAL)
|
.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 IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||||
private const val LOCATION_URI_KEY = "location_uri" // String
|
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 com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
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.CategoriesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||||
@ -52,7 +49,7 @@ class BackupCreator(
|
|||||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
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
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
file = (
|
file = (
|
||||||
@ -73,19 +70,18 @@ class BackupCreator(
|
|||||||
UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
?: throw Exception(context.stringResource(MR.strings.create_backup_file_error))
|
|
||||||
|
|
||||||
if (!file.isFile) {
|
if (file == null || !file.isFile) {
|
||||||
throw IllegalStateException("Failed to get handle on a backup file")
|
throw IllegalStateException(context.stringResource(MR.strings.create_backup_file_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
val databaseManga = getFavorites.await()
|
val databaseManga = getFavorites.await()
|
||||||
val backup = Backup(
|
val backup = Backup(
|
||||||
backupManga = backupMangas(databaseManga, flags),
|
backupManga = backupMangas(databaseManga, options),
|
||||||
backupCategories = backupCategories(flags),
|
backupCategories = backupCategories(options),
|
||||||
backupSources = backupSources(databaseManga),
|
backupSources = backupSources(databaseManga),
|
||||||
backupPreferences = backupAppPreferences(flags),
|
backupPreferences = backupAppPreferences(options),
|
||||||
backupSourcePreferences = backupSourcePreferences(flags),
|
backupSourcePreferences = backupSourcePreferences(options),
|
||||||
)
|
)
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
@ -118,30 +114,30 @@ class BackupCreator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun backupCategories(options: Int): List<BackupCategory> {
|
suspend fun backupCategories(options: BackupOptions): List<BackupCategory> {
|
||||||
if (options and BACKUP_CATEGORY != BACKUP_CATEGORY) return emptyList()
|
if (!options.categories) return emptyList()
|
||||||
|
|
||||||
return categoriesBackupCreator.backupCategories()
|
return categoriesBackupCreator.backupCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
suspend fun backupMangas(mangas: List<Manga>, options: BackupOptions): List<BackupManga> {
|
||||||
return mangaBackupCreator.backupMangas(mangas, flags)
|
return mangaBackupCreator.backupMangas(mangas, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
fun backupSources(mangas: List<Manga>): List<BackupSource> {
|
||||||
return sourcesBackupCreator.backupSources(mangas)
|
return sourcesBackupCreator.backupSources(mangas)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||||
if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList()
|
if (!options.appSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.backupAppPreferences()
|
return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||||
if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList()
|
if (!options.sourceSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.backupSourcePreferences()
|
return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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
|
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.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
@ -20,18 +20,17 @@ class MangaBackupCreator(
|
|||||||
private val getHistory: GetHistory = Injekt.get(),
|
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 {
|
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
|
// Entry for this manga
|
||||||
val mangaObject = manga.toBackupManga()
|
val mangaObject = manga.toBackupManga()
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
if (options.chapters) {
|
||||||
if (options and BackupCreateFlags.BACKUP_CHAPTER == BackupCreateFlags.BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
// Backup all the chapters
|
||||||
handler.awaitList {
|
handler.awaitList {
|
||||||
chaptersQueries.getChaptersByMangaId(
|
chaptersQueries.getChaptersByMangaId(
|
||||||
@ -44,8 +43,7 @@ class MangaBackupCreator(
|
|||||||
?.let { mangaObject.chapters = it }
|
?.let { mangaObject.chapters = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
if (options.categories) {
|
||||||
if (options and BackupCreateFlags.BACKUP_CATEGORY == BackupCreateFlags.BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
// Backup categories for this manga
|
||||||
val categoriesForManga = getCategories.await(manga.id)
|
val categoriesForManga = getCategories.await(manga.id)
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
@ -53,16 +51,14 @@ class MangaBackupCreator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
if (options.tracking) {
|
||||||
if (options and BackupCreateFlags.BACKUP_TRACK == BackupCreateFlags.BACKUP_TRACK) {
|
|
||||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
||||||
if (tracks.isNotEmpty()) {
|
if (tracks.isNotEmpty()) {
|
||||||
mangaObject.tracking = tracks
|
mangaObject.tracking = tracks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
if (options.history) {
|
||||||
if (options and BackupCreateFlags.BACKUP_HISTORY == BackupCreateFlags.BACKUP_HISTORY) {
|
|
||||||
val historyByMangaId = getHistory.await(manga.id)
|
val historyByMangaId = getHistory.await(manga.id)
|
||||||
if (historyByMangaId.isNotEmpty()) {
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
val history = historyByMangaId.map { history ->
|
val history = historyByMangaId.map { history ->
|
||||||
|
@ -22,26 +22,27 @@ class PreferenceBackupCreator(
|
|||||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun backupAppPreferences(): List<BackupPreference> {
|
fun backupAppPreferences(includePrivatePreferences: Boolean): List<BackupPreference> {
|
||||||
return preferenceStore.getAll().toBackupPreferences()
|
return preferenceStore.getAll().toBackupPreferences()
|
||||||
|
.withPrivatePreferences(includePrivatePreferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(): List<BackupSourcePreferences> {
|
fun backupSourcePreferences(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
|
||||||
return sourceManager.getCatalogueSources()
|
return sourceManager.getCatalogueSources()
|
||||||
.filterIsInstance<ConfigurableSource>()
|
.filterIsInstance<ConfigurableSource>()
|
||||||
.map {
|
.map {
|
||||||
BackupSourcePreferences(
|
BackupSourcePreferences(
|
||||||
it.preferenceKey(),
|
it.preferenceKey(),
|
||||||
it.sourcePreferences().all.toBackupPreferences(),
|
it.sourcePreferences().all.toBackupPreferences()
|
||||||
|
.withPrivatePreferences(includePrivatePreferences),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
||||||
return this.filterKeys {
|
return this
|
||||||
!Preference.isPrivate(it) && !Preference.isAppState(it)
|
.filterKeys { !Preference.isAppState(it) }
|
||||||
}
|
|
||||||
.mapNotNull { (key, value) ->
|
.mapNotNull { (key, value) ->
|
||||||
when (value) {
|
when (value) {
|
||||||
is Int -> BackupPreference(key, IntPreferenceValue(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 androidx.work.workDataOf
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
@ -30,8 +32,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||||
val options = inputData.getBooleanArray(OPTIONS_KEY)
|
val options: RestoreOptions? = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
|
||||||
?.let { RestoreOptions.fromBooleanArray(it) }
|
|
||||||
|
|
||||||
if (uri == null || options == null) {
|
if (uri == null || options == null) {
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
@ -84,7 +85,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val inputData = workDataOf(
|
val inputData = workDataOf(
|
||||||
LOCATION_URI_KEY to uri.toString(),
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
SYNC_KEY to sync,
|
SYNC_KEY to sync,
|
||||||
OPTIONS_KEY to options.toBooleanArray(),
|
OPTIONS_KEY to options.asBooleanArray(),
|
||||||
)
|
)
|
||||||
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
|
@ -4,14 +4,4 @@ data class RestoreOptions(
|
|||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val library: 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],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -304,7 +304,7 @@ internal object ExtensionLoader {
|
|||||||
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
|
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
|
||||||
is Source -> listOf(obj)
|
is Source -> listOf(obj)
|
||||||
is SourceFactory -> obj.createSources()
|
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) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
|
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.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
|
import eu.kanade.presentation.components.SEARCH_DEBOUNCE_MILLIS
|
||||||
@ -34,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
class ExtensionsScreenModel(
|
class ExtensionsScreenModel(
|
||||||
preferences: SourcePreferences = Injekt.get(),
|
preferences: SourcePreferences = Injekt.get(),
|
||||||
|
basePreferences: BasePreferences = Injekt.get(),
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
private val getExtensions: GetExtensionsByType = Injekt.get(),
|
||||||
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
|
) : StateScreenModel<ExtensionsScreenModel.State>(State()) {
|
||||||
@ -124,6 +126,10 @@ class ExtensionsScreenModel(
|
|||||||
preferences.extensionUpdatesCount().changes()
|
preferences.extensionUpdatesCount().changes()
|
||||||
.onEach { mutableState.update { state -> state.copy(updates = it) } }
|
.onEach { mutableState.update { state -> state.copy(updates = it) } }
|
||||||
.launchIn(screenModelScope)
|
.launchIn(screenModelScope)
|
||||||
|
|
||||||
|
basePreferences.extensionInstaller().changes()
|
||||||
|
.onEach { mutableState.update { state -> state.copy(installer = it) } }
|
||||||
|
.launchIn(screenModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
@ -199,6 +205,7 @@ class ExtensionsScreenModel(
|
|||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val items: ItemGroups = mutableMapOf(),
|
val items: ItemGroups = mutableMapOf(),
|
||||||
val updates: Int = 0,
|
val updates: Int = 0,
|
||||||
|
val installer: BasePreferences.ExtensionInstaller? = null,
|
||||||
val searchQuery: String? = null,
|
val searchQuery: String? = null,
|
||||||
) {
|
) {
|
||||||
val isEmpty = items.isEmpty()
|
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.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@ -167,3 +168,14 @@ fun Context.isInstalledFromFDroid(): Boolean {
|
|||||||
// F-Droid builds typically disable the updater
|
// F-Droid builds typically disable the updater
|
||||||
(!BuildConfig.INCLUDE_UPDATER && !isDevFlavor)
|
(!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)
|
||||||
|
}
|
||||||
|
@ -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_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_action_select">Select a folder</string>
|
||||||
<string name="onboarding_storage_selection_required">A folder must be selected</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">Install apps permission</string>
|
||||||
<string name="onboarding_permission_install_apps_description">To install source extensions.</string>
|
<string name="onboarding_permission_install_apps_description">To install source extensions.</string>
|
||||||
<string name="onboarding_permission_notifications">Notification permission</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_info_age_rating">Age rating</string>
|
||||||
<string name="ext_nsfw_short">18+</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_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_install_service_notif">Installing extension…</string>
|
||||||
<string name="ext_installer_pref">Installer</string>
|
<string name="ext_installer_pref">Installer</string>
|
||||||
<string name="ext_installer_legacy">Legacy</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="backup_choice">What do you want to backup?</string>
|
||||||
<string name="app_settings">App settings</string>
|
<string name="app_settings">App settings</string>
|
||||||
<string name="source_settings">Source 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">Creating backup</string>
|
||||||
<string name="creating_backup_error">Backup failed</string>
|
<string name="creating_backup_error">Backup failed</string>
|
||||||
<string name="missing_storage_permission">Storage permissions not granted</string>
|
<string name="missing_storage_permission">Storage permissions not granted</string>
|
||||||
|
@ -473,4 +473,4 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
override fun getFilterList() = FilterList()
|
override fun getFilterList() = FilterList()
|
||||||
}
|
}
|
||||||
|
|
||||||
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")
|
class LicensedMangaChaptersException : RuntimeException()
|
||||||
|
Loading…
Reference in New Issue
Block a user