mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
chore: merge upstream.
This commit is contained in:
commit
18fb63cd01
3
.github/workflows/build_pull_request.yml
vendored
3
.github/workflows/build_pull_request.yml
vendored
@ -3,7 +3,8 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'i18n/src/main/res/**/strings.xml'
|
||||
- 'i18n/src/commonMain/resources/**/strings.xml'
|
||||
- 'i18n/src/commonMain/resources/**/plurals.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
|
@ -22,7 +22,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
|
||||
versionCode = 112
|
||||
versionCode = 113
|
||||
versionName = "0.14.7"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
@ -123,6 +123,7 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
|
@ -160,7 +160,8 @@
|
||||
|
||||
<service
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="shortService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
@ -62,15 +62,15 @@ private const val GridSelectedCoverAlpha = 0.76f
|
||||
*/
|
||||
@Composable
|
||||
fun MangaCompactGridItem(
|
||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
isSelected: Boolean = false,
|
||||
title: String? = null,
|
||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
coverAlpha: Float = 1f,
|
||||
coverBadgeStart: @Composable (RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
) {
|
||||
GridItemSelectable(
|
||||
isSelected = isSelected,
|
||||
@ -163,15 +163,15 @@ private fun BoxScope.CoverTextOverlay(
|
||||
*/
|
||||
@Composable
|
||||
fun MangaComfortableGridItem(
|
||||
isSelected: Boolean = false,
|
||||
title: String,
|
||||
titleMaxLines: Int = 2,
|
||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
isSelected: Boolean = false,
|
||||
titleMaxLines: Int = 2,
|
||||
coverAlpha: Float = 1f,
|
||||
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
|
||||
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
) {
|
||||
GridItemSelectable(
|
||||
@ -253,10 +253,10 @@ private fun MangaGridCover(
|
||||
|
||||
@Composable
|
||||
private fun GridItemTitle(
|
||||
modifier: Modifier,
|
||||
title: String,
|
||||
style: TextStyle,
|
||||
minLines: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
maxLines: Int = 2,
|
||||
) {
|
||||
Text(
|
||||
@ -276,10 +276,10 @@ private fun GridItemTitle(
|
||||
*/
|
||||
@Composable
|
||||
private fun GridItemSelectable(
|
||||
modifier: Modifier = Modifier,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
@ -316,13 +316,13 @@ private fun Modifier.selectedOutline(
|
||||
*/
|
||||
@Composable
|
||||
fun MangaListItem(
|
||||
isSelected: Boolean = false,
|
||||
title: String,
|
||||
coverData: tachiyomi.domain.manga.model.MangaCover,
|
||||
coverAlpha: Float = 1f,
|
||||
badge: @Composable (RowScope.() -> Unit),
|
||||
onLongClick: () -> Unit,
|
||||
title: String,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
badge: @Composable (RowScope.() -> Unit),
|
||||
isSelected: Boolean = false,
|
||||
coverAlpha: Float = 1f,
|
||||
onClickContinueReading: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
|
@ -33,7 +33,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@ -189,7 +188,7 @@ fun MangaChapterListItem(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(ReadItemAlpha),
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ fun MangaInfoBox(
|
||||
)
|
||||
}
|
||||
.blur(4.dp)
|
||||
.alpha(.2f),
|
||||
.alpha(0.2f),
|
||||
)
|
||||
|
||||
// Manga & source info
|
||||
|
@ -35,10 +35,8 @@ import tachiyomi.presentation.core.theme.active
|
||||
|
||||
@Composable
|
||||
fun MangaToolbar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
titleAlphaProvider: () -> Float,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
hasFilters: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onClickFilter: () -> Unit,
|
||||
@ -47,10 +45,14 @@ fun MangaToolbar(
|
||||
onClickEditCategory: (() -> Unit)?,
|
||||
onClickRefresh: () -> Unit,
|
||||
onClickMigrate: (() -> Unit)?,
|
||||
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
@ -62,7 +64,7 @@ fun MangaToolbar(
|
||||
text = if (isActionMode) actionModeCounter.toString() else title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.alpha(if (isActionMode) 1f else titleAlphaProvider()),
|
||||
color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
|
@ -17,34 +17,38 @@ import eu.kanade.presentation.theme.TachiyomiTheme
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
internal fun GuidesStep(
|
||||
onRestoreBackup: () -> Unit,
|
||||
) {
|
||||
val handler = LocalUriHandler.current
|
||||
internal class GuidesStep(
|
||||
private val onRestoreBackup: () -> Unit,
|
||||
) : OnboardingStep {
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { handler.openUri(GETTING_STARTED_URL) },
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val handler = LocalUriHandler.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(stringResource(MR.strings.getting_started_guide))
|
||||
}
|
||||
Text(stringResource(MR.strings.onboarding_guides_new_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { handler.openUri(GETTING_STARTED_URL) },
|
||||
) {
|
||||
Text(stringResource(MR.strings.getting_started_guide))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
|
||||
Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onRestoreBackup,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
Text(stringResource(MR.strings.onboarding_guides_returning_user, stringResource(MR.strings.app_name)))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onRestoreBackup,
|
||||
) {
|
||||
Text(stringResource(MR.strings.pref_restore_backup))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,6 +61,6 @@ private fun GuidesStepPreview() {
|
||||
TachiyomiTheme {
|
||||
GuidesStep(
|
||||
onRestoreBackup = {},
|
||||
)
|
||||
).Content()
|
||||
}
|
||||
}
|
||||
|
@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
|
||||
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
storagePreferences: StoragePreferences,
|
||||
uiPreferences: UiPreferences,
|
||||
onComplete: () -> Unit,
|
||||
onRestoreBackup: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val slideDistance = rememberSlideDistance()
|
||||
|
||||
var currentStep by remember { mutableIntStateOf(0) }
|
||||
val steps: List<@Composable () -> Unit> = remember {
|
||||
var currentStep by rememberSaveable { mutableIntStateOf(0) }
|
||||
val steps = remember {
|
||||
listOf(
|
||||
{ ThemeStep(uiPreferences = uiPreferences) },
|
||||
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) },
|
||||
// TODO: prompt for notification permissions when bumping target to Android 13
|
||||
{ GuidesStep(onRestoreBackup = onRestoreBackup) },
|
||||
ThemeStep(),
|
||||
StorageStep(),
|
||||
PermissionStep(),
|
||||
GuidesStep(onRestoreBackup = onRestoreBackup),
|
||||
)
|
||||
}
|
||||
val isLastStep = currentStep == steps.size - 1
|
||||
val isLastStep = currentStep == steps.lastIndex
|
||||
|
||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
||||
|
||||
@ -61,16 +55,12 @@ fun OnboardingScreen(
|
||||
MR.strings.onboarding_action_next
|
||||
},
|
||||
),
|
||||
canAccept = steps[currentStep].isComplete,
|
||||
onAcceptClick = {
|
||||
if (isLastStep) {
|
||||
onComplete()
|
||||
} else {
|
||||
// TODO: this is kind of janky
|
||||
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
|
||||
context.toast(MR.strings.onboarding_storage_selection_required)
|
||||
} else {
|
||||
currentStep++
|
||||
}
|
||||
currentStep++
|
||||
}
|
||||
},
|
||||
) {
|
||||
@ -91,7 +81,7 @@ fun OnboardingScreen(
|
||||
},
|
||||
label = "stepContent",
|
||||
) {
|
||||
steps[it]()
|
||||
steps[it].Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.presentation.more.onboarding
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
internal interface OnboardingStep {
|
||||
|
||||
val isComplete: Boolean
|
||||
|
||||
@Composable
|
||||
fun Content()
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package eu.kanade.presentation.more.onboarding
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
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
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
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
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
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
|
||||
} else {
|
||||
true
|
||||
}
|
||||
batteryGranted = context.getSystemService<PowerManager>()!!
|
||||
.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
) {
|
||||
SectionHeader(stringResource(MR.strings.onboarding_permission_type_required))
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
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(),
|
||||
onResult = {
|
||||
// no-op. resulting checks is being done on resume
|
||||
},
|
||||
)
|
||||
PermissionItem(
|
||||
title = stringResource(MR.strings.onboarding_permission_notifications),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
|
||||
granted = notificationGranted,
|
||||
onButtonClick = { permissionRequester.launch(Manifest.permission.POST_NOTIFICATIONS) },
|
||||
)
|
||||
}
|
||||
|
||||
PermissionItem(
|
||||
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
|
||||
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
|
||||
granted = batteryGranted,
|
||||
onButtonClick = {
|
||||
@SuppressLint("BatteryLife")
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.secondaryItemAlpha(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionItem(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
granted: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onButtonClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
headlineContent = { Text(text = title) },
|
||||
supportingContent = { Text(text = subtitle) },
|
||||
trailingContent = {
|
||||
OutlinedButton(
|
||||
enabled = !granted,
|
||||
onClick = onButtonClick,
|
||||
) {
|
||||
if (granted) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Text(stringResource(MR.strings.onboarding_permission_action_grant))
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
@ -7,46 +7,66 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import tachiyomi.core.preference.Preference
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Button
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
internal fun StorageStep(
|
||||
storagePref: Preference<String>,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||
internal class StorageStep : OnboardingStep {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.onboarding_storage_info,
|
||||
stringResource(MR.strings.app_name),
|
||||
SettingsDataScreen.storageLocationText(storagePref),
|
||||
),
|
||||
)
|
||||
private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
},
|
||||
private var _isComplete by mutableStateOf(false)
|
||||
|
||||
override val isComplete: Boolean
|
||||
get() = _isComplete
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
||||
Text(
|
||||
stringResource(
|
||||
MR.strings.onboarding_storage_info,
|
||||
stringResource(MR.strings.app_name),
|
||||
SettingsDataScreen.storageLocationText(storagePref),
|
||||
),
|
||||
)
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
try {
|
||||
pickStorageLocation.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(MR.strings.file_picker_error)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(MR.strings.onboarding_storage_action_select))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
storagePref.changes()
|
||||
.collectLatest { _isComplete = storagePref.isSet() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,33 +8,40 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@Composable
|
||||
internal fun ThemeStep(
|
||||
uiPreferences: UiPreferences,
|
||||
) {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
internal class ThemeStep : OnboardingStep {
|
||||
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
override val isComplete: Boolean = true
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoled by amoledPref.collectAsState()
|
||||
private val uiPreferences: UiPreferences = Injekt.get()
|
||||
|
||||
Column {
|
||||
AppThemeModePreferenceWidget(
|
||||
value = themeMode,
|
||||
onItemClick = {
|
||||
themeModePref.set(it)
|
||||
setAppCompatDelegateThemeMode(it)
|
||||
},
|
||||
)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val themeModePref = uiPreferences.themeMode()
|
||||
val themeMode by themeModePref.collectAsState()
|
||||
|
||||
AppThemePreferenceWidget(
|
||||
value = appTheme,
|
||||
amoled = amoled,
|
||||
onItemClick = { appThemePref.set(it) },
|
||||
)
|
||||
val appThemePref = uiPreferences.appTheme()
|
||||
val appTheme by appThemePref.collectAsState()
|
||||
|
||||
val amoledPref = uiPreferences.themeDarkAmoled()
|
||||
val amoled by amoledPref.collectAsState()
|
||||
|
||||
Column {
|
||||
AppThemeModePreferenceWidget(
|
||||
value = themeMode,
|
||||
onItemClick = {
|
||||
themeModePref.set(it)
|
||||
setAppCompatDelegateThemeMode(it)
|
||||
},
|
||||
)
|
||||
|
||||
AppThemePreferenceWidget(
|
||||
value = appTheme,
|
||||
amoled = amoled,
|
||||
onItemClick = { appThemePref.set(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,9 +37,9 @@ import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
@ -107,7 +107,6 @@ object SettingsDataScreen : SearchableSettings {
|
||||
UniFile.fromUri(context, uri)?.let {
|
||||
storageDirPref.set(it.uri.toString())
|
||||
}
|
||||
Injekt.get<DownloadCache>().invalidateCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
|
@ -248,7 +248,6 @@ private fun TrackDetailsItem(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable(onClick = onClick)
|
||||
.alpha(if (text == null) UnsetStatusTextAlpha else 1f)
|
||||
.fillMaxHeight()
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
@ -259,7 +258,7 @@ private fun TrackDetailsItem(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UnsetStatusTextAlpha else 1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
@ -66,10 +66,6 @@ object Migrations {
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
if (oldVersion < 14) {
|
||||
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 15) {
|
||||
// Delete internal chapter cache dir.
|
||||
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
|
||||
@ -96,11 +92,6 @@ object Migrations {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 43) {
|
||||
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 44) {
|
||||
// Reset sorting preference if using removed sort by source
|
||||
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
|
||||
@ -259,9 +250,6 @@ object Migrations {
|
||||
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
|
||||
}
|
||||
}
|
||||
if (oldVersion < 76) {
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 77) {
|
||||
val oldReaderTap = prefs.getBoolean("reader_tap", false)
|
||||
if (!oldReaderTap) {
|
||||
@ -374,9 +362,6 @@ object Migrations {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (oldVersion < 100) {
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion < 105) {
|
||||
val pref = libraryPreferences.autoUpdateDeviceRestrictions()
|
||||
if (pref.isSet() && "battery_not_low" in pref.get()) {
|
||||
@ -396,12 +381,7 @@ object Migrations {
|
||||
newKey = { Preference.privateKey(it) },
|
||||
)
|
||||
}
|
||||
if (oldVersion < 111) {
|
||||
File(context.cacheDir, "dl_index_cache")
|
||||
.takeIf { it.exists() }
|
||||
?.delete()
|
||||
}
|
||||
if (oldVersion < 112) {
|
||||
if (oldVersion < 113) {
|
||||
val prefsToReplace = listOf(
|
||||
"pref_download_only",
|
||||
"incognito_mode",
|
||||
@ -421,6 +401,9 @@ object Migrations {
|
||||
filterPredicate = { it.key in prefsToReplace },
|
||||
newKey = { Preference.appStateKey(it) },
|
||||
)
|
||||
|
||||
// Deleting old download cache index files, but might as well clear it all out
|
||||
context.cacheDir.deleteRecursively()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -68,18 +68,18 @@ class BackupNotifier(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun showBackupComplete(unifile: UniFile) {
|
||||
fun showBackupComplete(file: UniFile) {
|
||||
context.cancelNotification(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
setContentTitle(context.stringResource(MR.strings.backup_created))
|
||||
setContentText(unifile.filePath ?: unifile.name)
|
||||
setContentText(file.filePath ?: file.name)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.stringResource(MR.strings.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
@ -88,13 +88,16 @@ class BackupNotifier(private val context: Context) {
|
||||
|
||||
fun showRestoreProgress(
|
||||
content: String = "",
|
||||
contentTitle: String = context.stringResource(
|
||||
MR.strings.restoring_backup,
|
||||
),
|
||||
progress: Int = 0,
|
||||
maxAmount: Int = 100,
|
||||
sync: Boolean = false,
|
||||
): NotificationCompat.Builder {
|
||||
val builder = with(progressNotificationBuilder) {
|
||||
val contentTitle = if (sync) {
|
||||
context.stringResource(MR.strings.syncing_library)
|
||||
} else {
|
||||
context.stringResource(MR.strings.restoring_backup)
|
||||
}
|
||||
setContentTitle(contentTitle)
|
||||
|
||||
if (!preferences.hideNotificationContent().get()) {
|
||||
@ -133,10 +136,14 @@ class BackupNotifier(private val context: Context) {
|
||||
errorCount: Int,
|
||||
path: String?,
|
||||
file: String?,
|
||||
contentTitle: String = context.stringResource(
|
||||
MR.strings.restore_completed,
|
||||
),
|
||||
sync: Boolean,
|
||||
) {
|
||||
val contentTitle = if (sync) {
|
||||
context.stringResource(MR.strings.library_sync_complete)
|
||||
} else {
|
||||
context.stringResource(MR.strings.restore_completed)
|
||||
}
|
||||
|
||||
context.cancelNotification(Notifications.ID_RESTORE_PROGRESS)
|
||||
|
||||
val timeString = context.stringResource(
|
||||
|
@ -1,679 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.source.model.copyFrom
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import eu.kanade.tachiyomi.util.BackupUtil
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.data.Manga_sync
|
||||
import tachiyomi.data.Mangas
|
||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.history.model.HistoryUpdate
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.sync.SyncPreferences
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
class BackupRestorer(
|
||||
private val context: Context,
|
||||
private val notifier: BackupNotifier,
|
||||
) {
|
||||
|
||||
private val handler: DatabaseHandler = Injekt.get()
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val getCategories: GetCategories = Injekt.get()
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get()
|
||||
private val fetchInterval: FetchInterval = Injekt.get()
|
||||
|
||||
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||
private val syncPreferences: SyncPreferences = Injekt.get()
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||
|
||||
private var restoreAmount = 0
|
||||
private var restoreProgress = 0
|
||||
|
||||
/**
|
||||
* Mapping of source ID to source name from backup data
|
||||
*/
|
||||
private var sourceMapping: Map<Long, String> = emptyMap()
|
||||
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
suspend fun syncFromBackup(uri: Uri, sync: Boolean) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
|
||||
performRestore(uri, sync)
|
||||
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
|
||||
val logFile = writeErrorLog()
|
||||
|
||||
if (sync) {
|
||||
syncPreferences.lastSyncTimestamp().set(Date().time)
|
||||
} else {
|
||||
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
file.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
}
|
||||
return File("")
|
||||
}
|
||||
|
||||
private suspend fun performRestore(uri: Uri, sync: Boolean) {
|
||||
val backup = BackupUtil.decodeBackup(context, uri)
|
||||
|
||||
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
|
||||
|
||||
// Store source mapping for error messages
|
||||
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||
now = ZonedDateTime.now()
|
||||
currentFetchWindow = fetchInterval.getWindow(now)
|
||||
|
||||
coroutineScope {
|
||||
ensureActive()
|
||||
restoreCategories(backup.backupCategories)
|
||||
|
||||
ensureActive()
|
||||
restoreAppPreferences(backup.backupPreferences)
|
||||
|
||||
ensureActive()
|
||||
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||
|
||||
// Restore individual manga
|
||||
backup.backupManga.forEach {
|
||||
ensureActive()
|
||||
restoreManga(it, backup.backupCategories, sync)
|
||||
}
|
||||
|
||||
// TODO: optionally trigger online library + tracker update
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||
if (backupCategories.isNotEmpty()) {
|
||||
val dbCategories = getCategories.await()
|
||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||
|
||||
val categories = backupCategories.map {
|
||||
dbCategoriesByName[it.name]
|
||||
?: handler.awaitOneExecutable {
|
||||
categoriesQueries.insert(it.name, it.order, it.flags)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}.let { id -> it.toCategory(id) }
|
||||
}
|
||||
|
||||
libraryPreferences.categorizedDisplaySettings().set(
|
||||
(dbCategories + categories)
|
||||
.distinctBy { it.flags }
|
||||
.size > 1,
|
||||
)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
context.stringResource(MR.strings.categories),
|
||||
context.stringResource(MR.strings.restoring_backup),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, sync: Boolean) {
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
val categories = backupManga.categories.map { it.toInt() }
|
||||
val history =
|
||||
backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + backupManga.history
|
||||
val tracks = backupManga.getTrackingImpl()
|
||||
|
||||
try {
|
||||
val dbManga = getMangaFromDatabase(manga.url, manga.source)
|
||||
val restoredManga = if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
// Manga in database
|
||||
// Copy information from manga already in database
|
||||
val updatedManga = restoreExistingManga(manga, dbManga)
|
||||
// Fetch rest of manga information
|
||||
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
||||
}
|
||||
updateManga.awaitUpdateFetchInterval(restoredManga, now, currentFetchWindow)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
if (sync) {
|
||||
showRestoreProgress(
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
manga.title,
|
||||
context.stringResource(MR.strings.syncing_library),
|
||||
)
|
||||
} else {
|
||||
showRestoreProgress(
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
manga.title,
|
||||
context.stringResource(MR.strings.restoring_backup),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns manga
|
||||
*
|
||||
* @return [Manga], null if not found
|
||||
*/
|
||||
private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
|
||||
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||
var updatedManga = manga.copy(id = dbManga._id)
|
||||
updatedManga = updatedManga.copyFrom(dbManga)
|
||||
updateManga(updatedManga)
|
||||
return updatedManga
|
||||
}
|
||||
|
||||
suspend fun updateManga(manga: Manga): Long {
|
||||
handler.await(true) {
|
||||
mangasQueries.update(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre?.joinToString(separator = ", "),
|
||||
title = manga.title,
|
||||
status = manga.status,
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = null,
|
||||
calculateInterval = null,
|
||||
initialized = manga.initialized,
|
||||
viewer = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
mangaId = manga.id,
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
)
|
||||
}
|
||||
return manga.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private suspend fun restoreExistingManga(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Manga {
|
||||
val fetchedManga = restoreNewManga(manga)
|
||||
restoreChapters(fetchedManga, chapters)
|
||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
private suspend fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
val dbChaptersByUrl = getChaptersByMangaId.await(manga.id)
|
||||
.associateBy { it.url }
|
||||
|
||||
val processed = chapters.map { chapter ->
|
||||
var updatedChapter = chapter
|
||||
|
||||
val dbChapter = dbChaptersByUrl[updatedChapter.url]
|
||||
if (dbChapter != null) {
|
||||
updatedChapter = updatedChapter
|
||||
.copyFrom(dbChapter)
|
||||
.copy(
|
||||
id = dbChapter.id,
|
||||
bookmark = updatedChapter.bookmark || dbChapter.bookmark,
|
||||
// Overwrite read status with the backup's status
|
||||
read = updatedChapter.read,
|
||||
)
|
||||
// Update lastPageRead if the chapter is marked as read
|
||||
if (updatedChapter.read) {
|
||||
updatedChapter = updatedChapter.copy(
|
||||
lastPageRead = if (updatedChapter.lastPageRead > 0) {
|
||||
updatedChapter.lastPageRead
|
||||
} else {
|
||||
dbChapter.lastPageRead
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updatedChapter.copy(mangaId = manga.id)
|
||||
}
|
||||
|
||||
val (existingChapters, newChapters) = processed.partition { it.id > 0 }
|
||||
updateKnownChapters(existingChapters)
|
||||
insertChapters(newChapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts list of chapters
|
||||
*/
|
||||
private suspend fun insertChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.insert(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read,
|
||||
chapter.bookmark,
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber,
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters with known database ids
|
||||
*/
|
||||
private suspend fun updateKnownChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.update(
|
||||
mangaId = null,
|
||||
url = null,
|
||||
name = null,
|
||||
scanlator = null,
|
||||
read = chapter.read,
|
||||
bookmark = chapter.bookmark,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
chapterNumber = null,
|
||||
sourceOrder = null,
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @return Updated manga info.
|
||||
*/
|
||||
private suspend fun restoreNewManga(manga: Manga): Manga {
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
private suspend fun insertManga(manga: Manga): Long {
|
||||
return handler.awaitOneExecutable(true) {
|
||||
mangasQueries.insert(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre,
|
||||
title = manga.title,
|
||||
status = manga.status,
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = 0L,
|
||||
calculateInterval = 0L,
|
||||
initialized = manga.initialized,
|
||||
viewerFlags = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreNewManga(
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Manga {
|
||||
restoreChapters(backupManga, chapters)
|
||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||
return backupManga
|
||||
}
|
||||
|
||||
private suspend fun restoreExtras(
|
||||
manga: Manga,
|
||||
categories: List<Int>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreHistory(history)
|
||||
restoreTracking(manga, tracks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories a manga is in.
|
||||
*
|
||||
* @param manga the manga whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
private suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||
val dbCategories = getCategories.await()
|
||||
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
||||
|
||||
categories.forEach { backupCategoryOrder ->
|
||||
backupCategories.firstOrNull {
|
||||
it.order == backupCategoryOrder.toLong()
|
||||
}?.let { backupCategory ->
|
||||
dbCategories.firstOrNull { dbCategory ->
|
||||
dbCategory.name == backupCategory.name
|
||||
}?.let { dbCategory ->
|
||||
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
handler.await(true) {
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore history from Json
|
||||
*
|
||||
* @param history list containing history to be restored
|
||||
*/
|
||||
private suspend fun restoreHistory(history: List<BackupHistory>) {
|
||||
// List containing history to be updated
|
||||
val toUpdate = mutableListOf<HistoryUpdate>()
|
||||
for ((url, lastRead, readDuration) in history) {
|
||||
var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
|
||||
// Check if history already in database and update
|
||||
if (dbHistory != null) {
|
||||
dbHistory = dbHistory.copy(
|
||||
last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
|
||||
time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||
)
|
||||
toUpdate.add(
|
||||
HistoryUpdate(
|
||||
chapterId = dbHistory.chapter_id,
|
||||
readAt = dbHistory.last_read!!,
|
||||
sessionReadDuration = dbHistory.time_read,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// If not in database create
|
||||
handler
|
||||
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
|
||||
?.let {
|
||||
toUpdate.add(
|
||||
HistoryUpdate(
|
||||
chapterId = it._id,
|
||||
readAt = Date(lastRead),
|
||||
sessionReadDuration = readDuration,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.await(true) {
|
||||
toUpdate.forEach { payload ->
|
||||
historyQueries.upsert(
|
||||
payload.chapterId,
|
||||
payload.readAt,
|
||||
payload.sessionReadDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param manga the manga whose sync have to be restored.
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
private suspend fun restoreTracking(manga: Manga, tracks: List<Track>) {
|
||||
// Get tracks from database
|
||||
val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
||||
val toUpdate = mutableListOf<Manga_sync>()
|
||||
val toInsert = mutableListOf<Track>()
|
||||
|
||||
tracks
|
||||
// Fix foreign keys with the current manga id
|
||||
.map { it.copy(mangaId = manga.id) }
|
||||
.forEach { track ->
|
||||
var isInDatabase = false
|
||||
for (dbTrack in dbTracks) {
|
||||
if (track.syncId == dbTrack.sync_id) {
|
||||
// The sync is already in the db, only update its fields
|
||||
var temp = dbTrack
|
||||
if (track.remoteId != dbTrack.remote_id) {
|
||||
temp = temp.copy(remote_id = track.remoteId)
|
||||
}
|
||||
if (track.libraryId != dbTrack.library_id) {
|
||||
temp = temp.copy(library_id = track.libraryId)
|
||||
}
|
||||
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
||||
isInDatabase = true
|
||||
toUpdate.add(temp)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isInDatabase) {
|
||||
// Insert new sync. Let the db assign the id
|
||||
toInsert.add(track.copy(id = 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (toUpdate.isNotEmpty()) {
|
||||
handler.await(true) {
|
||||
toUpdate.forEach { track ->
|
||||
manga_syncQueries.update(
|
||||
track.manga_id,
|
||||
track.sync_id,
|
||||
track.remote_id,
|
||||
track.library_id,
|
||||
track.title,
|
||||
track.last_chapter_read,
|
||||
track.total_chapters,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remote_url,
|
||||
track.start_date,
|
||||
track.finish_date,
|
||||
track._id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toInsert.isNotEmpty()) {
|
||||
handler.await(true) {
|
||||
toInsert.forEach { track ->
|
||||
manga_syncQueries.insert(
|
||||
track.mangaId,
|
||||
track.syncId,
|
||||
track.remoteId,
|
||||
track.libraryId,
|
||||
track.title,
|
||||
track.lastChapterRead,
|
||||
track.totalChapters,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
||||
restorePreferences(preferences, preferenceStore)
|
||||
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreateJob.setupTask(context)
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
context.stringResource(MR.strings.app_settings),
|
||||
context.stringResource(MR.strings.restoring_backup),
|
||||
)
|
||||
}
|
||||
|
||||
private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
|
||||
preferences.forEach {
|
||||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||
restorePreferences(it.prefs, sourcePrefs)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
context.stringResource(MR.strings.source_settings),
|
||||
context.stringResource(MR.strings.restoring_backup),
|
||||
)
|
||||
}
|
||||
|
||||
private fun restorePreferences(
|
||||
toRestore: List<BackupPreference>,
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
val prefs = preferenceStore.getAll()
|
||||
toRestore.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is IntPreferenceValue -> {
|
||||
if (prefs[key] is Int?) {
|
||||
preferenceStore.getInt(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is LongPreferenceValue -> {
|
||||
if (prefs[key] is Long?) {
|
||||
preferenceStore.getLong(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is FloatPreferenceValue -> {
|
||||
if (prefs[key] is Float?) {
|
||||
preferenceStore.getFloat(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringPreferenceValue -> {
|
||||
if (prefs[key] is String?) {
|
||||
preferenceStore.getString(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is BooleanPreferenceValue -> {
|
||||
if (prefs[key] is Boolean?) {
|
||||
preferenceStore.getBoolean(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringSetPreferenceValue -> {
|
||||
if (prefs[key] is Set<*>?) {
|
||||
preferenceStore.getStringSet(key).set(value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRestoreProgress(progress: Int, amount: Int, title: String, contentTitle: String) {
|
||||
notifier.showRestoreProgress(title, contentTitle, progress, amount)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
package eu.kanade.tachiyomi.data.backup.create
|
||||
|
||||
internal object BackupCreateFlags {
|
||||
const val BACKUP_CATEGORY = 0x1
|
@ -1,7 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
package eu.kanade.tachiyomi.data.backup.create
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
@ -14,6 +16,8 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
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.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
@ -68,6 +72,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_BACKUP_PROGRESS,
|
||||
notifier.showBackupProgress().build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
package eu.kanade.tachiyomi.data.backup.create
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK
|
||||
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_CHAPTER
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_TRACK
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.history.model.History
|
||||
import java.util.Date
|
||||
|
||||
@Serializable
|
||||
data class BackupHistory(
|
||||
@ProtoNumber(1) var url: String,
|
||||
@ProtoNumber(2) var lastRead: Long,
|
||||
@ProtoNumber(3) var readDuration: Long = 0,
|
||||
)
|
||||
) {
|
||||
fun getHistoryImpl(): History {
|
||||
return History.create().copy(
|
||||
readAt = Date(lastRead),
|
||||
readDuration = readDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.")
|
||||
@Serializable
|
||||
@ -16,4 +25,8 @@ data class BrokenBackupHistory(
|
||||
@ProtoNumber(0) var url: String,
|
||||
@ProtoNumber(1) var lastRead: Long,
|
||||
@ProtoNumber(2) var readDuration: Long = 0,
|
||||
)
|
||||
) {
|
||||
fun toBackupHistory(): BackupHistory {
|
||||
return BackupHistory(url, lastRead, readDuration)
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Serializable
|
||||
@ -63,18 +61,6 @@ data class BackupManga(
|
||||
)
|
||||
}
|
||||
|
||||
fun getChaptersImpl(): List<Chapter> {
|
||||
return chapters.map {
|
||||
it.toChapterImpl()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrackingImpl(): List<Track> {
|
||||
return tracking.map {
|
||||
it.getTrackingImpl()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun copyFrom(manga: Manga): BackupManga {
|
||||
return BackupManga(
|
||||
|
@ -8,7 +8,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
data class BrokenBackupSource(
|
||||
@ProtoNumber(0) var name: String = "",
|
||||
@ProtoNumber(1) var sourceId: Long,
|
||||
)
|
||||
) {
|
||||
fun toBackupSource() = BackupSource(name, sourceId)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BackupSource(
|
||||
|
@ -30,7 +30,7 @@ data class BackupTracking(
|
||||
) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getTrackingImpl(): Track {
|
||||
fun getTrackImpl(): Track {
|
||||
return Track(
|
||||
id = -1,
|
||||
mangaId = -1,
|
||||
|
@ -1,7 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
package eu.kanade.tachiyomi.data.backup.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
@ -9,6 +11,7 @@ import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
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.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isRunning
|
||||
@ -28,13 +31,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
||||
override suspend fun doWork(): Result {
|
||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||
?: return Result.failure()
|
||||
val sync = inputData.getBoolean(SYNC_KEY, false)
|
||||
val isSync = inputData.getBoolean(SYNC_KEY, false)
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
return try {
|
||||
val restorer = BackupRestorer(context, notifier)
|
||||
restorer.syncFromBackup(uri, sync)
|
||||
BackupRestorer(context, notifier, isSync).restore(uri)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
@ -54,6 +56,11 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_RESTORE_PROGRESS,
|
||||
notifier.showRestoreProgress().build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,156 @@
|
||||
package eu.kanade.tachiyomi.data.backup.restore
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.util.BackupUtil
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class BackupRestorer(
|
||||
private val context: Context,
|
||||
private val notifier: BackupNotifier,
|
||||
private val isSync: Boolean,
|
||||
|
||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||
private val mangaRestorer: MangaRestorer = MangaRestorer(),
|
||||
) {
|
||||
|
||||
private var restoreAmount = 0
|
||||
private var restoreProgress = 0
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
/**
|
||||
* Mapping of source ID to source name from backup data
|
||||
*/
|
||||
private var sourceMapping: Map<Long, String> = emptyMap()
|
||||
|
||||
suspend fun restore(uri: Uri) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
restoreFromFile(uri)
|
||||
|
||||
val time = System.currentTimeMillis() - startTime
|
||||
|
||||
val logFile = writeErrorLog()
|
||||
|
||||
notifier.showRestoreComplete(
|
||||
time,
|
||||
errors.size,
|
||||
logFile.parent,
|
||||
logFile.name,
|
||||
isSync,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun restoreFromFile(uri: Uri) {
|
||||
val backup = BackupUtil.decodeBackup(context, uri)
|
||||
|
||||
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
|
||||
|
||||
// Store source mapping for error messages
|
||||
val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
|
||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||
|
||||
coroutineScope {
|
||||
restoreCategories(backup.backupCategories)
|
||||
restoreAppPreferences(backup.backupPreferences)
|
||||
restoreSourcePreferences(backup.backupSourcePreferences)
|
||||
restoreManga(backup.backupManga, backup.backupCategories)
|
||||
|
||||
// TODO: optionally trigger online library + tracker update
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreCategories(backupCategories: List<BackupCategory>) = launch {
|
||||
ensureActive()
|
||||
categoriesRestorer.restoreCategories(backupCategories)
|
||||
|
||||
restoreProgress += 1
|
||||
notifier.showRestoreProgress(
|
||||
context.stringResource(MR.strings.categories),
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
isSync,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreManga(
|
||||
backupMangas: List<BackupManga>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) = launch {
|
||||
mangaRestorer.sortByNew(backupMangas)
|
||||
.forEach {
|
||||
ensureActive()
|
||||
|
||||
try {
|
||||
mangaRestorer.restoreManga(it, backupCategories)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[it.source] ?: it.source.toString()
|
||||
errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
notifier.showRestoreProgress(it.title, restoreProgress, restoreAmount, isSync)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
|
||||
ensureActive()
|
||||
preferenceRestorer.restoreAppPreferences(preferences)
|
||||
|
||||
restoreProgress += 1
|
||||
notifier.showRestoreProgress(
|
||||
context.stringResource(MR.strings.app_settings),
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
isSync,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreSourcePreferences(preferences: List<BackupSourcePreferences>) = launch {
|
||||
ensureActive()
|
||||
preferenceRestorer.restoreSourcePreferences(preferences)
|
||||
|
||||
restoreProgress += 1
|
||||
notifier.showRestoreProgress(
|
||||
context.stringResource(MR.strings.source_settings),
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
isSync,
|
||||
)
|
||||
}
|
||||
|
||||
private fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
file.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
}
|
||||
return File("")
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package eu.kanade.tachiyomi.data.backup.restore
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class CategoriesRestorer(
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||
if (backupCategories.isNotEmpty()) {
|
||||
val dbCategories = getCategories.await()
|
||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||
|
||||
val categories = backupCategories.map {
|
||||
dbCategoriesByName[it.name]
|
||||
?: handler.awaitOneExecutable {
|
||||
categoriesQueries.insert(it.name, it.order, it.flags)
|
||||
categoriesQueries.selectLastInsertedRowId()
|
||||
}.let { id -> it.toCategory(id) }
|
||||
}
|
||||
|
||||
libraryPreferences.categorizedDisplaySettings().set(
|
||||
(dbCategories + categories)
|
||||
.distinctBy { it.flags }
|
||||
.size > 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,402 @@
|
||||
package eu.kanade.tachiyomi.data.backup.restore
|
||||
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import kotlin.math.max
|
||||
|
||||
class MangaRestorer(
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
private val insertTrack: InsertTrack = Injekt.get(),
|
||||
fetchInterval: FetchInterval = Injekt.get(),
|
||||
) {
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||
|
||||
init {
|
||||
now = ZonedDateTime.now()
|
||||
currentFetchWindow = fetchInterval.getWindow(now)
|
||||
}
|
||||
|
||||
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
|
||||
val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() }
|
||||
.groupBy({ it.source }, { it.url })
|
||||
|
||||
return backupMangas
|
||||
.sortedWith(
|
||||
compareBy<BackupManga> { it.url in urlsBySource[it.source].orEmpty() }
|
||||
.then(compareByDescending { it.lastModifiedAt }),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun restoreManga(
|
||||
backupManga: BackupManga,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
val dbManga = findExistingManga(backupManga)
|
||||
val manga = backupManga.getMangaImpl()
|
||||
val restoredManga = if (dbManga == null) {
|
||||
restoreNewManga(manga)
|
||||
} else {
|
||||
restoreExistingManga(manga, dbManga)
|
||||
}
|
||||
|
||||
restoreMangaDetails(
|
||||
manga = restoredManga,
|
||||
chapters = backupManga.chapters,
|
||||
categories = backupManga.categories,
|
||||
backupCategories = backupCategories,
|
||||
history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
|
||||
tracks = backupManga.tracking,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun findExistingManga(backupManga: BackupManga): Manga? {
|
||||
return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source)
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga {
|
||||
return if (manga.lastModifiedAt > dbManga.lastModifiedAt) {
|
||||
updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id))
|
||||
} else {
|
||||
updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id))
|
||||
}
|
||||
}
|
||||
|
||||
private fun Manga.copyFrom(newer: Manga): Manga {
|
||||
return this.copy(
|
||||
favorite = this.favorite || newer.favorite,
|
||||
author = newer.author,
|
||||
artist = newer.artist,
|
||||
description = newer.description,
|
||||
genre = newer.genre,
|
||||
thumbnailUrl = newer.thumbnailUrl,
|
||||
status = newer.status,
|
||||
initialized = this.initialized || newer.initialized,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateManga(manga: Manga): Manga {
|
||||
handler.await(true) {
|
||||
mangasQueries.update(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre?.joinToString(separator = ", "),
|
||||
title = manga.title,
|
||||
status = manga.status,
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = null,
|
||||
calculateInterval = null,
|
||||
initialized = manga.initialized,
|
||||
viewer = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
mangaId = manga.id,
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
)
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private suspend fun restoreNewManga(
|
||||
manga: Manga,
|
||||
): Manga {
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun restoreChapters(manga: Manga, backupChapters: List<BackupChapter>) {
|
||||
val dbChaptersByUrl = getChaptersByMangaId.await(manga.id)
|
||||
.associateBy { it.url }
|
||||
|
||||
val (existingChapters, newChapters) = backupChapters
|
||||
.mapNotNull {
|
||||
val chapter = it.toChapterImpl().copy(mangaId = manga.id)
|
||||
|
||||
val dbChapter = dbChaptersByUrl[chapter.url]
|
||||
?: // New chapter
|
||||
return@mapNotNull chapter
|
||||
|
||||
if (chapter.forComparison() == dbChapter.forComparison()) {
|
||||
// Same state; skip
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
// Update to an existing chapter
|
||||
var updatedChapter = chapter
|
||||
.copyFrom(dbChapter)
|
||||
.copy(
|
||||
id = dbChapter.id,
|
||||
bookmark = chapter.bookmark || dbChapter.bookmark,
|
||||
)
|
||||
if (dbChapter.read && !updatedChapter.read) {
|
||||
updatedChapter = updatedChapter.copy(
|
||||
read = true,
|
||||
lastPageRead = dbChapter.lastPageRead,
|
||||
)
|
||||
} else if (updatedChapter.lastPageRead == 0L && dbChapter.lastPageRead != 0L) {
|
||||
updatedChapter = updatedChapter.copy(
|
||||
lastPageRead = dbChapter.lastPageRead,
|
||||
)
|
||||
}
|
||||
updatedChapter
|
||||
}
|
||||
.partition { it.id > 0 }
|
||||
|
||||
insertNewChapters(newChapters)
|
||||
updateExistingChapters(existingChapters)
|
||||
}
|
||||
|
||||
private fun Chapter.forComparison() =
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L)
|
||||
|
||||
private suspend fun insertNewChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.insert(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read,
|
||||
chapter.bookmark,
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber,
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateExistingChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
chapters.forEach { chapter ->
|
||||
chaptersQueries.update(
|
||||
mangaId = null,
|
||||
url = null,
|
||||
name = null,
|
||||
scanlator = null,
|
||||
read = chapter.read,
|
||||
bookmark = chapter.bookmark,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
chapterNumber = null,
|
||||
sourceOrder = null,
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts manga and returns id
|
||||
*
|
||||
* @return id of [Manga], null if not found
|
||||
*/
|
||||
private suspend fun insertManga(manga: Manga): Long {
|
||||
return handler.awaitOneExecutable(true) {
|
||||
mangasQueries.insert(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre,
|
||||
title = manga.title,
|
||||
status = manga.status,
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = 0L,
|
||||
calculateInterval = 0L,
|
||||
initialized = manga.initialized,
|
||||
viewerFlags = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreMangaDetails(
|
||||
manga: Manga,
|
||||
chapters: List<BackupChapter>,
|
||||
categories: List<Long>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
history: List<BackupHistory>,
|
||||
tracks: List<BackupTracking>,
|
||||
): Manga {
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreChapters(manga, chapters)
|
||||
restoreTracking(manga, tracks)
|
||||
restoreHistory(history)
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
|
||||
return manga
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories a manga is in.
|
||||
*
|
||||
* @param manga the manga whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
private suspend fun restoreCategories(
|
||||
manga: Manga,
|
||||
categories: List<Long>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
val dbCategories = getCategories.await()
|
||||
val dbCategoriesByName = dbCategories.associateBy { it.name }
|
||||
|
||||
val backupCategoriesByOrder = backupCategories.associateBy { it.order }
|
||||
|
||||
val mangaCategoriesToUpdate = categories.mapNotNull { backupCategoryOrder ->
|
||||
backupCategoriesByOrder[backupCategoryOrder]?.let { backupCategory ->
|
||||
dbCategoriesByName[backupCategory.name]?.let { dbCategory ->
|
||||
Pair(manga.id, dbCategory.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||
handler.await(true) {
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
|
||||
val toUpdate = backupHistory.mapNotNull { history ->
|
||||
val dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(history.url) }
|
||||
val item = history.getHistoryImpl()
|
||||
|
||||
if (dbHistory == null) {
|
||||
val chapter = handler.awaitOneOrNull { chaptersQueries.getChapterByUrl(history.url) }
|
||||
return@mapNotNull if (chapter == null) {
|
||||
// Chapter doesn't exist; skip
|
||||
null
|
||||
} else {
|
||||
// New history entry
|
||||
item.copy(chapterId = chapter._id)
|
||||
}
|
||||
}
|
||||
|
||||
// Update history entry
|
||||
item.copy(
|
||||
id = dbHistory._id,
|
||||
chapterId = dbHistory.chapter_id,
|
||||
readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L)
|
||||
.takeIf { it > 0L }
|
||||
?.let { Date(it) },
|
||||
readDuration = max(item.readDuration, dbHistory.time_read),
|
||||
)
|
||||
}
|
||||
|
||||
if (toUpdate.isNotEmpty()) {
|
||||
handler.await(true) {
|
||||
toUpdate.forEach {
|
||||
historyQueries.upsert(
|
||||
it.chapterId,
|
||||
it.readAt,
|
||||
it.readDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun restoreTracking(manga: Manga, backupTracks: List<BackupTracking>) {
|
||||
val dbTrackBySyncId = getTracks.await(manga.id).associateBy { it.syncId }
|
||||
|
||||
val (existingTracks, newTracks) = backupTracks
|
||||
.mapNotNull {
|
||||
val track = it.getTrackImpl()
|
||||
val dbTrack = dbTrackBySyncId[track.syncId]
|
||||
?: // New track
|
||||
return@mapNotNull track.copy(
|
||||
id = 0, // Let DB assign new ID
|
||||
mangaId = manga.id,
|
||||
)
|
||||
|
||||
if (track.forComparison() == dbTrack.forComparison()) {
|
||||
// Same state; skip
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
// Update to an existing track
|
||||
dbTrack.copy(
|
||||
remoteId = track.remoteId,
|
||||
libraryId = track.libraryId,
|
||||
lastChapterRead = max(dbTrack.lastChapterRead, track.lastChapterRead),
|
||||
)
|
||||
}
|
||||
.partition { it.id > 0 }
|
||||
|
||||
if (newTracks.isNotEmpty()) {
|
||||
insertTrack.awaitAll(newTracks)
|
||||
}
|
||||
if (existingTracks.isNotEmpty()) {
|
||||
handler.await(true) {
|
||||
existingTracks.forEach { track ->
|
||||
manga_syncQueries.update(
|
||||
track.mangaId,
|
||||
track.syncId,
|
||||
track.remoteId,
|
||||
track.libraryId,
|
||||
track.title,
|
||||
track.lastChapterRead,
|
||||
track.totalChapters,
|
||||
track.status,
|
||||
track.score,
|
||||
track.remoteUrl,
|
||||
track.startDate,
|
||||
track.finishDate,
|
||||
track.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package eu.kanade.tachiyomi.data.backup.restore
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||
import tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class PreferenceRestorer(
|
||||
private val context: Context,
|
||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
||||
) {
|
||||
|
||||
fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
||||
restorePreferences(preferences, preferenceStore)
|
||||
|
||||
LibraryUpdateJob.setupTask(context)
|
||||
BackupCreateJob.setupTask(context)
|
||||
}
|
||||
|
||||
fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
|
||||
preferences.forEach {
|
||||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||
restorePreferences(it.prefs, sourcePrefs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restorePreferences(
|
||||
toRestore: List<BackupPreference>,
|
||||
preferenceStore: PreferenceStore,
|
||||
) {
|
||||
val prefs = preferenceStore.getAll()
|
||||
toRestore.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is IntPreferenceValue -> {
|
||||
if (prefs[key] is Int?) {
|
||||
preferenceStore.getInt(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is LongPreferenceValue -> {
|
||||
if (prefs[key] is Long?) {
|
||||
preferenceStore.getLong(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is FloatPreferenceValue -> {
|
||||
if (prefs[key] is Float?) {
|
||||
preferenceStore.getFloat(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringPreferenceValue -> {
|
||||
if (prefs[key] is String?) {
|
||||
preferenceStore.getString(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is BooleanPreferenceValue -> {
|
||||
if (prefs[key] is Boolean?) {
|
||||
preferenceStore.getBoolean(key).set(value.value)
|
||||
}
|
||||
}
|
||||
is StringSetPreferenceValue -> {
|
||||
if (prefs[key] is Set<*>?) {
|
||||
preferenceStore.getStringSet(key).set(value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
@ -48,7 +47,7 @@ import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.domain.storage.service.StorageManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
@ -66,7 +65,7 @@ class DownloadCache(
|
||||
private val provider: DownloadProvider = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
storagePreferences: StoragePreferences = Injekt.get(),
|
||||
private val storageManager: StorageManager = Injekt.get(),
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
@ -74,7 +73,7 @@ class DownloadCache(
|
||||
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
|
||||
val changes = _changes.receiveAsFlow()
|
||||
.onStart { emit(Unit) }
|
||||
.shareIn(scope, SharingStarted.Eagerly, 1)
|
||||
.shareIn(scope, SharingStarted.Lazily, 1)
|
||||
|
||||
/**
|
||||
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
|
||||
@ -94,10 +93,10 @@ class DownloadCache(
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
|
||||
|
||||
private val diskCacheFile: File
|
||||
get() = File(context.cacheDir, "dl_index_cache_v2")
|
||||
get() = File(context.cacheDir, "dl_index_cache_v3")
|
||||
|
||||
private val rootDownloadsDirLock = Mutex()
|
||||
private var rootDownloadsDir = RootDirectory(provider.downloadsDir)
|
||||
private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
|
||||
init {
|
||||
// Attempt to read cache file
|
||||
@ -115,12 +114,8 @@ class DownloadCache(
|
||||
}
|
||||
}
|
||||
|
||||
storagePreferences.baseStorageDirectory().changes()
|
||||
.drop(1)
|
||||
.onEach {
|
||||
rootDownloadsDir = RootDirectory(provider.downloadsDir)
|
||||
invalidateCache()
|
||||
}
|
||||
storageManager.changes
|
||||
.onEach { invalidateCache() }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
@ -294,6 +289,8 @@ class DownloadCache(
|
||||
fun invalidateCache() {
|
||||
lastRenew = 0L
|
||||
renewalJob?.cancel()
|
||||
diskCacheFile.delete()
|
||||
renewCache()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -310,23 +307,26 @@ class DownloadCache(
|
||||
_isInitializing.emit(true)
|
||||
}
|
||||
|
||||
var sources = getSources()
|
||||
|
||||
// Try to wait until extensions and sources have loaded
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
while (!extensionManager.isInitialized) {
|
||||
delay(2.seconds)
|
||||
}
|
||||
var sources = getSources()
|
||||
if (sources.isEmpty()) {
|
||||
withTimeoutOrNull(30.seconds) {
|
||||
while (!extensionManager.isInitialized) {
|
||||
delay(2.seconds)
|
||||
}
|
||||
|
||||
while (sources.isEmpty()) {
|
||||
delay(2.seconds)
|
||||
sources = getSources()
|
||||
while (extensionManager.availableExtensionsFlow.value.isNotEmpty() && sources.isEmpty()) {
|
||||
delay(2.seconds)
|
||||
sources = getSources()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||
|
||||
rootDownloadsDirLock.withLock {
|
||||
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
|
||||
|
||||
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
|
||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||
.mapNotNull { dir ->
|
||||
@ -371,10 +371,9 @@ class DownloadCache(
|
||||
}.also {
|
||||
it.invokeOnCompletion(onCancelling = true) { exception ->
|
||||
if (exception != null && exception !is CancellationException) {
|
||||
logcat(LogPriority.ERROR, exception) { "Failed to create download cache" }
|
||||
logcat(LogPriority.ERROR, exception) { "DownloadCache: failed to create cache" }
|
||||
}
|
||||
lastRenew = System.currentTimeMillis()
|
||||
|
||||
notifyChanges()
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,17 @@ import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||
import eu.kanade.tachiyomi.util.system.isOnline
|
||||
import eu.kanade.tachiyomi.util.system.NetworkState
|
||||
import eu.kanade.tachiyomi.util.system.activeNetworkState
|
||||
import eu.kanade.tachiyomi.util.system.networkStateFlow
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -50,7 +54,11 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
var active = checkConnectivity() && downloadManager.downloaderStart()
|
||||
var networkCheck = checkNetworkState(
|
||||
applicationContext.activeNetworkState(),
|
||||
downloadPreferences.downloadOnlyOverWifi().get(),
|
||||
)
|
||||
var active = networkCheck && downloadManager.downloaderStart()
|
||||
|
||||
if (!active) {
|
||||
return Result.failure()
|
||||
@ -58,29 +66,36 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
coroutineScope {
|
||||
combineTransform(
|
||||
applicationContext.networkStateFlow(),
|
||||
downloadPreferences.downloadOnlyOverWifi().changes(),
|
||||
transform = { a, b -> emit(checkNetworkState(a, b)) },
|
||||
)
|
||||
.onEach { networkCheck = it }
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
// Keep the worker running when needed
|
||||
while (active) {
|
||||
delay(100)
|
||||
active = !isStopped && downloadManager.isRunning && checkConnectivity()
|
||||
active = !isStopped && downloadManager.isRunning && networkCheck
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun checkConnectivity(): Boolean {
|
||||
return with(applicationContext) {
|
||||
if (isOnline()) {
|
||||
val noWifi = downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()
|
||||
if (noWifi) {
|
||||
downloadManager.downloaderStop(
|
||||
applicationContext.getString(R.string.download_notifier_text_only_wifi),
|
||||
)
|
||||
}
|
||||
!noWifi
|
||||
} else {
|
||||
downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network))
|
||||
false
|
||||
private fun checkNetworkState(state: NetworkState, requireWifi: Boolean): Boolean {
|
||||
return if (state.isOnline) {
|
||||
val noWifi = requireWifi && !state.isWifi
|
||||
if (noWifi) {
|
||||
downloadManager.downloaderStop(
|
||||
applicationContext.getString(R.string.download_notifier_text_only_wifi),
|
||||
)
|
||||
}
|
||||
!noWifi
|
||||
} else {
|
||||
downloadManager.downloaderStop(applicationContext.getString(R.string.download_notifier_no_network))
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.updater
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
@ -55,6 +57,11 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_APP_UPDATER,
|
||||
notifier.onDownloadStarted().build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.IntentSanitizer
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
@ -25,6 +26,20 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
||||
?.run {
|
||||
// Doesn't actually needed as the receiver is actually not exported
|
||||
// But the warnings can't be suppressed without this
|
||||
IntentSanitizer.Builder()
|
||||
.allowAction(this.action!!)
|
||||
.allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second }
|
||||
.allowAnyComponent()
|
||||
.allowPackage {
|
||||
// There is no way to check the actual installer name so allow all.
|
||||
true
|
||||
}
|
||||
.build()
|
||||
.sanitizeByFiltering(this)
|
||||
}
|
||||
if (userAction == null) {
|
||||
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
|
||||
continueQueue(InstallStep.Error)
|
||||
@ -71,13 +86,13 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
activeSession!!.second,
|
||||
Intent(INSTALL_ACTION),
|
||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||
).intentSender
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
activeSession?.let { (_, sessionId) ->
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
}
|
||||
@ -105,7 +120,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||
service,
|
||||
packageActionReceiver,
|
||||
IntentFilter(INSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_EXPORTED,
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import tachiyomi.data.Mangas
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
fun Manga.copyFrom(other: Mangas): Manga {
|
||||
var manga = this
|
||||
other.author?.let { manga = manga.copy(author = it) }
|
||||
other.artist?.let { manga = manga.copy(artist = it) }
|
||||
other.description?.let { manga = manga.copy(description = it) }
|
||||
other.genre?.let { manga = manga.copy(genre = it) }
|
||||
other.thumbnail_url?.let { manga = manga.copy(thumbnailUrl = it) }
|
||||
manga = manga.copy(status = other.status)
|
||||
if (!initialized) {
|
||||
manga = manga.copy(initialized = other.initialized)
|
||||
}
|
||||
return manga
|
||||
}
|
@ -65,8 +65,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
|
||||
.fillMaxSize()
|
||||
.padding(contentPadding),
|
||||
) {
|
||||
val fragment = SourcePreferencesFragment.getInstance(sourceId)
|
||||
add(it, fragment, null)
|
||||
add(it, SourcePreferencesFragment.getInstance(sourceId), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -127,26 +126,28 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private fun populateScreen(): PreferenceScreen {
|
||||
val sourceId = requireArguments().getLong(SOURCE_ID)
|
||||
val source = Injekt.get<SourceManager>().get(sourceId)!! as ConfigurableSource
|
||||
|
||||
val dataStore = SharedPreferencesDataStore(source.sourcePreferences())
|
||||
preferenceManager.preferenceDataStore = dataStore
|
||||
|
||||
val source = Injekt.get<SourceManager>().getOrStub(sourceId)
|
||||
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||
source.setupPreferenceScreen(sourceScreen)
|
||||
sourceScreen.forEach { pref ->
|
||||
pref.isIconSpaceReserved = false
|
||||
pref.isSingleLineTitle = false
|
||||
if (pref is DialogPreference && pref.dialogTitle.isNullOrEmpty()) {
|
||||
pref.dialogTitle = pref.title
|
||||
}
|
||||
|
||||
// Apply incognito IME for EditTextPreference
|
||||
if (pref is EditTextPreference) {
|
||||
val setListener = pref.getOnBindEditTextListener()
|
||||
pref.setOnBindEditTextListener {
|
||||
setListener?.onBindEditText(it)
|
||||
it.setIncognito(lifecycleScope)
|
||||
if (source is ConfigurableSource) {
|
||||
val dataStore = SharedPreferencesDataStore(source.sourcePreferences())
|
||||
preferenceManager.preferenceDataStore = dataStore
|
||||
|
||||
source.setupPreferenceScreen(sourceScreen)
|
||||
sourceScreen.forEach { pref ->
|
||||
pref.isIconSpaceReserved = false
|
||||
pref.isSingleLineTitle = false
|
||||
if (pref is DialogPreference && pref.dialogTitle.isNullOrEmpty()) {
|
||||
pref.dialogTitle = pref.title
|
||||
}
|
||||
|
||||
// Apply incognito IME for EditTextPreference
|
||||
if (pref is EditTextPreference) {
|
||||
val setListener = pref.getOnBindEditTextListener()
|
||||
pref.setOnBindEditTextListener {
|
||||
setListener?.onBindEditText(it)
|
||||
it.setIncognito(lifecycleScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,9 +159,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
|
||||
private const val SOURCE_ID = "source_id"
|
||||
|
||||
fun getInstance(sourceId: Long): SourcePreferencesFragment {
|
||||
val fragment = SourcePreferencesFragment()
|
||||
fragment.arguments = bundleOf(SOURCE_ID to sourceId)
|
||||
return fragment
|
||||
return SourcePreferencesFragment().apply {
|
||||
arguments = bundleOf(SOURCE_ID to sourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ class DeepLinkScreenModel(
|
||||
}
|
||||
|
||||
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga {
|
||||
return getMangaByUrlAndSourceId.awaitManga(sManga.url, sourceId)
|
||||
return getMangaByUrlAndSourceId.await(sManga.url, sourceId)
|
||||
?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationRailItem
|
||||
@ -277,7 +278,12 @@ object HomeScreen : Screen() {
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(painter = tab.options.icon!!, contentDescription = tab.options.title)
|
||||
Icon(
|
||||
painter = tab.options.icon!!,
|
||||
contentDescription = tab.options.title,
|
||||
// TODO: https://issuetracker.google.com/u/0/issues/316327367
|
||||
tint = LocalContentColor.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,7 +349,7 @@ class MainActivity : BaseActivity() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!preferences.shownOnboardingFlow().get()) {
|
||||
if (!preferences.shownOnboardingFlow().get() && navigator.lastItem !is OnboardingScreen) {
|
||||
navigator.push(OnboardingScreen())
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ class MangaScreen(
|
||||
)
|
||||
}.takeIf { isHttpSource },
|
||||
onTrackingClicked = {
|
||||
if (successState.trackingCount == 0) {
|
||||
if (screenModel.loggedInTrackers.isEmpty()) {
|
||||
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
|
||||
} else {
|
||||
screenModel.showTrackDialog()
|
||||
|
@ -118,7 +118,7 @@ class MangaScreenModel(
|
||||
private val successState: State.Success?
|
||||
get() = state.value as? State.Success
|
||||
|
||||
private val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } }
|
||||
val loggedInTrackers by lazy { trackerManager.trackers.filter { it.isLoggedIn } }
|
||||
|
||||
val manga: Manga?
|
||||
get() = successState?.manga
|
||||
@ -636,18 +636,18 @@ class MangaScreenModel(
|
||||
) {
|
||||
val successState = successState ?: return
|
||||
|
||||
if (startNow) {
|
||||
val chapterId = chapters.singleOrNull()?.id ?: return
|
||||
downloadManager.startDownloadNow(chapterId)
|
||||
} else {
|
||||
downloadChapters(chapters)
|
||||
}
|
||||
|
||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||
updateSuccessState { state ->
|
||||
state.copy(hasPromptedToAddBefore = true)
|
||||
screenModelScope.launchNonCancellable {
|
||||
if (startNow) {
|
||||
val chapterId = chapters.singleOrNull()?.id ?: return@launchNonCancellable
|
||||
downloadManager.startDownloadNow(chapterId)
|
||||
} else {
|
||||
downloadChapters(chapters)
|
||||
}
|
||||
screenModelScope.launch {
|
||||
|
||||
if (!isFavorited && !successState.hasPromptedToAddBefore) {
|
||||
updateSuccessState { state ->
|
||||
state.copy(hasPromptedToAddBefore = true)
|
||||
}
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = context.stringResource(MR.strings.snack_add_to_library),
|
||||
actionLabel = context.stringResource(MR.strings.action_add),
|
||||
|
@ -1,15 +1,16 @@
|
||||
package eu.kanade.tachiyomi.ui.more
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.more.onboarding.OnboardingScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsScreen
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@ -20,18 +21,22 @@ class OnboardingScreen : Screen() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val basePreferences = remember { Injekt.get<BasePreferences>() }
|
||||
val storagePreferences = remember { Injekt.get<StoragePreferences>() }
|
||||
val uiPreferences = remember { Injekt.get<UiPreferences>() }
|
||||
val shownOnboardingFlow by basePreferences.shownOnboardingFlow().collectAsState()
|
||||
|
||||
val finishOnboarding = {
|
||||
val finishOnboarding: () -> Unit = {
|
||||
basePreferences.shownOnboardingFlow().set(true)
|
||||
navigator.pop()
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = !shownOnboardingFlow,
|
||||
onBack = {
|
||||
// Prevent exiting if onboarding hasn't been completed
|
||||
},
|
||||
)
|
||||
|
||||
OnboardingScreen(
|
||||
storagePreferences = storagePreferences,
|
||||
uiPreferences = uiPreferences,
|
||||
onComplete = { finishOnboarding() },
|
||||
onComplete = finishOnboarding,
|
||||
onRestoreBackup = {
|
||||
finishOnboarding()
|
||||
navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage))
|
||||
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.backup.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||
import okio.buffer
|
||||
|
@ -0,0 +1,67 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
data class NetworkState(
|
||||
val isConnected: Boolean,
|
||||
val isValidated: Boolean,
|
||||
val isWifi: Boolean,
|
||||
) {
|
||||
val isOnline = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
isConnected && isValidated
|
||||
} else {
|
||||
isConnected
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Context.activeNetworkState(): NetworkState {
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return NetworkState(
|
||||
isConnected = connectivityManager.activeNetworkInfo?.isConnected ?: false,
|
||||
isValidated = capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false,
|
||||
isWifi = wifiManager.isWifiEnabled && capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Context.networkStateFlow() = callbackFlow {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val networkCallback = object : NetworkCallback() {
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
trySend(activeNetworkState())
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
trySend(activeNetworkState())
|
||||
}
|
||||
}
|
||||
|
||||
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
} else {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
|
||||
trySend(activeNetworkState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
|
||||
awaitClose {
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
object AndroidConfig {
|
||||
const val compileSdk = 34
|
||||
const val minSdk = 23
|
||||
const val targetSdk = 30
|
||||
const val ndk = "22.1.7171670"
|
||||
const val targetSdk = 34
|
||||
const val ndk = "26.1.10909125"
|
||||
}
|
||||
|
35
data/src/main/java/tachiyomi/data/track/TrackMapper.kt
Normal file
35
data/src/main/java/tachiyomi/data/track/TrackMapper.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package tachiyomi.data.track
|
||||
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
||||
object TrackMapper {
|
||||
fun mapTrack(
|
||||
id: Long,
|
||||
mangaId: Long,
|
||||
syncId: Long,
|
||||
remoteId: Long,
|
||||
libraryId: Long?,
|
||||
title: String,
|
||||
lastChapterRead: Double,
|
||||
totalChapters: Long,
|
||||
status: Long,
|
||||
score: Double,
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
): Track = Track(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
syncId = syncId,
|
||||
remoteId = remoteId,
|
||||
libraryId = libraryId,
|
||||
title = title,
|
||||
lastChapterRead = lastChapterRead,
|
||||
totalChapters = totalChapters,
|
||||
status = status,
|
||||
score = score,
|
||||
remoteUrl = remoteUrl,
|
||||
startDate = startDate,
|
||||
finishDate = finishDate,
|
||||
)
|
||||
}
|
@ -10,24 +10,24 @@ class TrackRepositoryImpl(
|
||||
) : TrackRepository {
|
||||
|
||||
override suspend fun getTrackById(id: Long): Track? {
|
||||
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, ::mapTrack) }
|
||||
return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) }
|
||||
}
|
||||
|
||||
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
||||
return handler.awaitList {
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack)
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTracksAsFlow(): Flow<List<Track>> {
|
||||
return handler.subscribeToList {
|
||||
manga_syncQueries.getTracks(::mapTrack)
|
||||
manga_syncQueries.getTracks(TrackMapper::mapTrack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
|
||||
return handler.subscribeToList {
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack)
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,34 +68,4 @@ class TrackRepositoryImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapTrack(
|
||||
id: Long,
|
||||
mangaId: Long,
|
||||
syncId: Long,
|
||||
remoteId: Long,
|
||||
libraryId: Long?,
|
||||
title: String,
|
||||
lastChapterRead: Double,
|
||||
totalChapters: Long,
|
||||
status: Long,
|
||||
score: Double,
|
||||
remoteUrl: String,
|
||||
startDate: Long,
|
||||
finishDate: Long,
|
||||
): Track = Track(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
syncId = syncId,
|
||||
remoteId = remoteId,
|
||||
libraryId = libraryId,
|
||||
title = title,
|
||||
lastChapterRead = lastChapterRead,
|
||||
totalChapters = totalChapters,
|
||||
status = status,
|
||||
score = score,
|
||||
remoteUrl = remoteUrl,
|
||||
startDate = startDate,
|
||||
finishDate = finishDate,
|
||||
)
|
||||
}
|
||||
|
@ -70,6 +70,10 @@ getAllManga:
|
||||
SELECT *
|
||||
FROM mangas;
|
||||
|
||||
getAllMangaSourceAndUrl:
|
||||
SELECT source, url
|
||||
FROM mangas;
|
||||
|
||||
getMangasWithFavoriteTimestamp:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
|
@ -7,4 +7,13 @@ data class History(
|
||||
val chapterId: Long,
|
||||
val readAt: Date?,
|
||||
val readDuration: Long,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
fun create() = History(
|
||||
id = -1L,
|
||||
chapterId = -1L,
|
||||
readAt = null,
|
||||
readDuration = -1L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import tachiyomi.domain.manga.repository.MangaRepository
|
||||
class GetMangaByUrlAndSourceId(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
suspend fun awaitManga(url: String, sourceId: Long): Manga? {
|
||||
suspend fun await(url: String, sourceId: Long): Manga? {
|
||||
return mangaRepository.getMangaByUrlAndSourceId(url, sourceId)
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,14 @@ import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
||||
class StorageManager(
|
||||
private val context: Context,
|
||||
@ -16,24 +22,33 @@ class StorageManager(
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var baseDir: UniFile? = storagePreferences.baseStorageDirectory().get().let(::getBaseDir)
|
||||
private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory().get())
|
||||
|
||||
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
|
||||
val changes = _changes.receiveAsFlow()
|
||||
.shareIn(scope, SharingStarted.Lazily, 1)
|
||||
|
||||
init {
|
||||
storagePreferences.baseStorageDirectory().changes()
|
||||
.onEach { baseDir = getBaseDir(it) }
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uri ->
|
||||
baseDir = getBaseDir(uri)
|
||||
baseDir?.let { parent ->
|
||||
parent.createDirectory(AUTOMATIC_BACKUPS_PATH)
|
||||
parent.createDirectory(LOCAL_SOURCE_PATH)
|
||||
parent.createDirectory(DOWNLOADS_PATH).also {
|
||||
DiskUtil.createNoMediaFile(it, context)
|
||||
}
|
||||
}
|
||||
_changes.send(Unit)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun getBaseDir(path: String): UniFile? {
|
||||
val file = UniFile.fromUri(context, path.toUri())
|
||||
|
||||
return file.takeIf { it?.exists() == true }?.also { parent ->
|
||||
parent.createDirectory(AUTOMATIC_BACKUPS_PATH)
|
||||
parent.createDirectory(LOCAL_SOURCE_PATH)
|
||||
parent.createDirectory(DOWNLOADS_PATH).also {
|
||||
DiskUtil.createNoMediaFile(it, context)
|
||||
}
|
||||
}
|
||||
private fun getBaseDir(uri: String): UniFile? {
|
||||
return UniFile.fromUri(context, uri.toUri())
|
||||
.takeIf { it?.exists() == true }
|
||||
}
|
||||
|
||||
fun getAutomaticBackupsDirectory(): UniFile? {
|
||||
|
@ -23,5 +23,4 @@ org.gradle.caching=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
|
||||
android.useAndroidX=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
@ -6,7 +6,7 @@ paging_version = "3.2.1"
|
||||
[libraries]
|
||||
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" }
|
||||
|
||||
annotation = "androidx.annotation:annotation:1.7.0"
|
||||
annotation = "androidx.annotation:annotation:1.7.1"
|
||||
appcompat = "androidx.appcompat:appcompat:1.6.1"
|
||||
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
@ -28,7 +28,7 @@ paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pag
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.2"
|
||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02"
|
||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha02"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha05"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01"
|
||||
|
||||
[bundles]
|
||||
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
|
||||
|
@ -1,10 +1,10 @@
|
||||
[versions]
|
||||
compiler = "1.5.6"
|
||||
compose-bom = "2023.12.00-alpha03"
|
||||
compose-bom = "2023.12.00-alpha04"
|
||||
accompanist = "0.33.2-alpha"
|
||||
|
||||
[libraries]
|
||||
activity = "androidx.activity:activity-compose:1.8.1"
|
||||
activity = "androidx.activity:activity-compose:1.8.2"
|
||||
bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
foundation = { module = "androidx.compose.foundation:foundation" }
|
||||
animation = { module = "androidx.compose.animation:animation" }
|
||||
|
@ -1,13 +1,13 @@
|
||||
[versions]
|
||||
kotlin_version = "1.9.21"
|
||||
serialization_version = "1.6.2"
|
||||
xml_serialization_version = "0.86.2"
|
||||
xml_serialization_version = "0.86.3"
|
||||
|
||||
[libraries]
|
||||
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
||||
gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
||||
|
||||
immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.6" }
|
||||
immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" }
|
||||
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.3" }
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||
|
@ -2,7 +2,7 @@
|
||||
aboutlib_version = "10.9.2"
|
||||
leakcanary = "2.12"
|
||||
moko = "0.23.0"
|
||||
okhttp_version = "5.0.0-alpha.11"
|
||||
okhttp_version = "5.0.0-alpha.12"
|
||||
richtext = "0.17.0"
|
||||
shizuku_version = "12.2.0"
|
||||
sqldelight = "2.0.0"
|
||||
@ -20,7 +20,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_ve
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
||||
okio = "com.squareup.okio:okio:3.6.0"
|
||||
okio = "com.squareup.okio:okio:3.7.0"
|
||||
|
||||
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
@ -53,7 +53,7 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
|
||||
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
|
||||
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
|
||||
|
||||
material = "com.google.android.material:material:1.10.0"
|
||||
material = "com.google.android.material:material:1.11.0"
|
||||
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
|
||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||
@ -94,7 +94,7 @@ voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", vers
|
||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
|
||||
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:12.0.2"
|
||||
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:12.0.3"
|
||||
google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197-1.25.0"
|
||||
google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
|
||||
|
||||
|
@ -306,9 +306,9 @@
|
||||
<string name="pref_category_security">الأمان و الخصوصية</string>
|
||||
<string name="pref_manage_notifications">أدر الإشعارات</string>
|
||||
<string name="pref_date_format">صيغة التاريخ</string>
|
||||
<string name="theme_system">اتبع مظهر النظام</string>
|
||||
<string name="theme_dark">مفعّل</string>
|
||||
<string name="theme_light">غير مفعّل</string>
|
||||
<string name="theme_system">النظام</string>
|
||||
<string name="theme_dark">داكن</string>
|
||||
<string name="theme_light">فاتح</string>
|
||||
<string name="battery_optimization_disabled">تم إلغاء وضع تحسين البطارية مُسبقاً</string>
|
||||
<string name="pref_disable_battery_optimization_summary">يساعد في عملية تحديث المكتبة والنسخ الإحتياطي في الخلفية</string>
|
||||
<string name="pref_disable_battery_optimization">إطفاء وضع تحسين البطارية</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="action_bar_up_description">اصعد</string>
|
||||
<string name="pref_storage_location">مكان التخزين</string>
|
||||
<string name="pref_storage_location_info">يُستخدَم في الاحتياط وتنزيل الفصول والمصدر المحليِّ.</string>
|
||||
<string name="onboarding_storage_action_select">حدِّد مجلَّدًا</string>
|
||||
<string name="pref_onboarding_guide">دليل البدء</string>
|
||||
<string name="onboarding_guides_new_user">أحديث العهد بـ%s؟ طالع دليل البدء.</string>
|
||||
<string name="onboarding_action_finish">ابدأ</string>
|
||||
<string name="onboarding_storage_selection_required">لا بد من تحديد مجلَّد</string>
|
||||
<string name="onboarding_heading">أهلًا وسهلًا!</string>
|
||||
<string name="onboarding_guides_returning_user">أستخدمتَ %s قبلًا؟</string>
|
||||
<string name="onboarding_action_skip">تخطَّ</string>
|
||||
<string name="onboarding_action_next">التالي</string>
|
||||
<string name="onboarding_description">أول أمرنا أن نضبط بعض الأمور، ولك أن تغيرها في الإعدادات لاحقًا.</string>
|
||||
<string name="no_location_set">لم يُعيَّن موضع للتخزين</string>
|
||||
<string name="onboarding_storage_info">حدِّد مجلَّدًا يُخزِّن فيه %1$s الفصول المنزَّلة والاحتياطات وغيرها.
|
||||
\n
|
||||
\nوالأحسن أن يكون المجلَّد مخصوصًا لذلك.
|
||||
\n
|
||||
\nالمجلَّد المحدَّد: %2$s</string>
|
||||
</resources>
|
@ -184,6 +184,15 @@
|
||||
<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>
|
||||
<string name="onboarding_permission_notifications_description">Get notified for library updates and more.</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Background battery usage</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Avoid interruptions to long-running library updates, downloads, and backup restores.</string>
|
||||
<string name="onboarding_permission_action_grant">Grant</string>
|
||||
<string name="onboarding_guides_new_user">New to %s? We recommend checking out the getting started guide.</string>
|
||||
<string name="onboarding_guides_returning_user">Already used %s before?</string>
|
||||
|
||||
|
@ -52,4 +52,16 @@
|
||||
<item quantity="one">পরবর্তী অপঠিত অধ্যায়</item>
|
||||
<item quantity="other">পরবর্তী %d টি অপঠিত অধ্যায়</item>
|
||||
</plurals>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="one">পরের %d চ্যাপ্টার</item>
|
||||
<item quantity="other">পরের %d চ্যাপ্টার</item>
|
||||
</plurals>
|
||||
<plurals name="missing_chapters">
|
||||
<item quantity="one">%1$s টি চ্যাপ্টার নেই</item>
|
||||
<item quantity="other">%1$s টি চ্যাপ্টার নেই</item>
|
||||
</plurals>
|
||||
<plurals name="day">
|
||||
<item quantity="one">%d দিন</item>
|
||||
<item quantity="other">%d দিন</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -615,4 +615,14 @@
|
||||
<string name="action_not_now">এখন না</string>
|
||||
<string name="pref_debug_info">ডিবাগ তথ্য</string>
|
||||
<string name="skipped_reason_not_always_update">বাদ দেওয়া হয়েছে কারণ সিরিজের আপডেটের প্রয়োজন নেই৷</string>
|
||||
<string name="unlock_app_title">আনলক %s</string>
|
||||
<string name="action_set_interval">সেট ইন্টারভেল</string>
|
||||
<string name="delete_downloaded">ডাউনলোড করা ফাইল ডিলেট করুন</string>
|
||||
<string name="action_menu_overflow_description">আর অপশন</string>
|
||||
<string name="selected">সিলেক্টেড</string>
|
||||
<string name="not_selected">নট সিলেক্টেড</string>
|
||||
<string name="scanlator">স্ক্যানলেটর</string>
|
||||
<string name="action_bar_up_description">নেভিগেট আপ</string>
|
||||
<string name="label_data_storage">ডাটা অন স্টোরেজ</string>
|
||||
<string name="action_filter_interval_custom">কাস্টমাইজড আনার ব্যবধান</string>
|
||||
</resources>
|
@ -301,9 +301,9 @@
|
||||
<string name="action_sort_latest_chapter">Darrer capítol</string>
|
||||
<string name="action_view_chapters">Mostra els capítols</string>
|
||||
<string name="action_cancel_all">Cancel·la-ho tot</string>
|
||||
<string name="theme_light">Desactivat</string>
|
||||
<string name="theme_dark">Activat</string>
|
||||
<string name="theme_system">Per defecte del sistema</string>
|
||||
<string name="theme_light">Clar</string>
|
||||
<string name="theme_dark">Fosc</string>
|
||||
<string name="theme_system">Sistema</string>
|
||||
<string name="pref_manage_notifications">Gestiona les notificacions</string>
|
||||
<string name="pref_category_security">Seguretat i privadesa</string>
|
||||
<string name="lock_with_biometrics">Requereix desblocatge</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="pref_storage_location_info">S’utilitza per a les còpies de seguretat automàtiques, les baixades de capítols i la font local.</string>
|
||||
<string name="action_menu_overflow_description">Més opcions</string>
|
||||
<string name="action_bar_up_description">Navega cap amunt</string>
|
||||
<string name="onboarding_storage_action_select">Selecciona una carpeta</string>
|
||||
<string name="pref_onboarding_guide">Guia de benvinguda</string>
|
||||
<string name="onboarding_guides_new_user">No heu fet servir mai el %s? Us recomanem que reviseu la guia de benvinguda.</string>
|
||||
<string name="onboarding_action_finish">Comença</string>
|
||||
<string name="onboarding_storage_selection_required">Cal que seleccioneu una carpeta</string>
|
||||
<string name="onboarding_heading">Et donem la benvinguda!</string>
|
||||
<string name="onboarding_guides_returning_user">Ja heu fet servir %s abans?</string>
|
||||
<string name="onboarding_action_skip">Omet</string>
|
||||
<string name="onboarding_action_next">Següent</string>
|
||||
<string name="onboarding_description">Primer cal configurar unes quantes coses. Sempre podràs canviar aquestes opcions a la configuració.</string>
|
||||
<string name="no_location_set">No s’ha definit una ubicació d’emmagatzematge</string>
|
||||
<string name="onboarding_storage_info">Seleccioneu una carpeta on el %1$s emmagatzemarà les baixades dels capítols, les còpies de seguretat i més.
|
||||
\n
|
||||
\nÉs recomanable fer servir una carpeta dedicada.
|
||||
\n
|
||||
\nCarpeta seleccionada: %2$s</string>
|
||||
</resources>
|
@ -301,9 +301,9 @@
|
||||
<string name="action_sort_latest_chapter">Neuestes Kapitel</string>
|
||||
<string name="action_view_chapters">Kapitel anzeigen</string>
|
||||
<string name="action_cancel_all">Alle abbrechen</string>
|
||||
<string name="theme_light">Aus</string>
|
||||
<string name="theme_dark">An</string>
|
||||
<string name="theme_system">Systemeinstellung</string>
|
||||
<string name="theme_light">Hell</string>
|
||||
<string name="theme_dark">Dunkel</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="pref_manage_notifications">Benachrichtigungen verwalten</string>
|
||||
<string name="pref_category_security">Sicherheit und Privatsphäre</string>
|
||||
<string name="lock_with_biometrics">Entsperren erforderlich</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="pref_storage_location_info">Wird für automatische Datensicherungen, heruntergeladene Kapitel und lokale Quellen verwendet.</string>
|
||||
<string name="action_menu_overflow_description">Weitere Optionen</string>
|
||||
<string name="action_bar_up_description">Nach oben navigieren</string>
|
||||
<string name="onboarding_storage_action_select">Ordner auswählen</string>
|
||||
<string name="pref_onboarding_guide">Einführungstour</string>
|
||||
<string name="onboarding_guides_new_user">Neu bei %s? Wir empfehlen dir, unseren Einstiegsleitfaden anzusehen.</string>
|
||||
<string name="onboarding_action_finish">Loslegen</string>
|
||||
<string name="onboarding_storage_selection_required">Es muss ein Ordner ausgewählt sein</string>
|
||||
<string name="onboarding_heading">Willkommen!</string>
|
||||
<string name="onboarding_guides_returning_user">%s bereits genutzt?</string>
|
||||
<string name="onboarding_action_skip">Überspringen</string>
|
||||
<string name="onboarding_action_next">Weiter</string>
|
||||
<string name="onboarding_description">Lass uns zuerst ein paar Dinge einrichten. Du kannst diese später in den Einstellungen jederzeit ändern.</string>
|
||||
<string name="no_location_set">Kein Speicherort festgelegt</string>
|
||||
<string name="onboarding_storage_info">Wähle einen Ordner aus, in welchem %1$s Kapitel-Downloads, Datensicherungen und mehr speichern wird.
|
||||
\n
|
||||
\nEin dedizierter Ordner wird empfohlen.
|
||||
\n
|
||||
\nAusgewählter Ordner: %2$s</string>
|
||||
</resources>
|
@ -347,9 +347,9 @@
|
||||
<string name="pref_category_security">Ασφάλεια και ιδιωτικότητα</string>
|
||||
<string name="pref_manage_notifications">Διαχείριση ειδοποιήσεων</string>
|
||||
<string name="pref_date_format">Μορφή ημερομηνίας</string>
|
||||
<string name="theme_system">Ακολουθήστε το σύστημα</string>
|
||||
<string name="theme_dark">Ενεργοποιημένο</string>
|
||||
<string name="theme_light">Απενεργοποιημένο</string>
|
||||
<string name="theme_system">Σύστημα</string>
|
||||
<string name="theme_dark">Σκοτεινό</string>
|
||||
<string name="theme_light">Φωτεινό</string>
|
||||
<string name="action_move_to_bottom">Μετακίνηση στον πάτο</string>
|
||||
<string name="action_move_to_top">Μετακίνηση στην κορυφή</string>
|
||||
<string name="action_cancel_all">Ακύρωση όλων</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="action_bar_up_description">Πλοήγηση προς τα πάνω</string>
|
||||
<string name="pref_storage_location">Τοποθεσία αποθήκευσης</string>
|
||||
<string name="pref_storage_location_info">Χρησιμοποιείται για αυτόματα αντίγραφα ασφαλείας, λήψη κεφαλαίων και τοπική πηγή.</string>
|
||||
<string name="onboarding_storage_action_select">Επιλέξτε ένα φάκελο</string>
|
||||
<string name="onboarding_action_finish">Ξεκινήστε</string>
|
||||
<string name="onboarding_storage_selection_required">Ένας φάκελος πρέπει να επιλεγεί</string>
|
||||
<string name="onboarding_heading">Καλώς ορίσατε!</string>
|
||||
<string name="onboarding_action_skip">Παράλειψη</string>
|
||||
<string name="onboarding_action_next">Επόμενο</string>
|
||||
<string name="pref_onboarding_guide">Οδηγός εισαγωγής</string>
|
||||
<string name="onboarding_guides_new_user">Είστε νέοι στο %s; Σας συνιστούμε να ανατρέξετε στον οδηγό έναρξης.</string>
|
||||
<string name="onboarding_guides_returning_user">Έχετε ξαναχρησιμοποιήσει το %s;</string>
|
||||
<string name="onboarding_description">Ας ρυθμίσουμε πρώτα κάποια πράγματα. Μπορείτε πάντα να τα αλλάξετε στις ρυθμίσεις αργότερα.</string>
|
||||
<string name="no_location_set">Δεν έχει οριστεί τοποθεσία αποθήκευσης</string>
|
||||
<string name="onboarding_storage_info">Επιλέξτε ένα φάκελο όπου το %1$s θα αποθηκεύει λήψεις κεφαλαίων, αντίγραφα ασφαλείας και άλλα.
|
||||
\n
|
||||
\nΣυνιστάται ένας αποκλειστικός φάκελος.
|
||||
\n
|
||||
\nΕπιλεγμένος φάκελος: %2$s</string>
|
||||
</resources>
|
@ -90,7 +90,7 @@
|
||||
<string name="pref_remove_after_marked_as_read">Borrarlos tras marcarlos como leídos de forma manual</string>
|
||||
<string name="pref_remove_after_read">Borrar capítulos terminados de forma automática</string>
|
||||
<string name="services">Servicios de seguimiento</string>
|
||||
<string name="pref_clear_chapter_cache">Vaciar la caché de capítulos</string>
|
||||
<string name="pref_clear_chapter_cache">Limpiar la caché de capítulos</string>
|
||||
<string name="used_cache">Usado: %1$s</string>
|
||||
<string name="cache_deleted">Se vació la caché. Se han eliminado %1$d archivos</string>
|
||||
<string name="cache_delete_error">Se produjo un error al limpiar</string>
|
||||
@ -302,9 +302,9 @@
|
||||
<string name="action_sort_latest_chapter">Por capítulo más reciente</string>
|
||||
<string name="action_view_chapters">Ver capítulos</string>
|
||||
<string name="action_cancel_all">Cancelar todo</string>
|
||||
<string name="theme_light">No</string>
|
||||
<string name="theme_dark">Sí</string>
|
||||
<string name="theme_system">Según ajustes del sistema</string>
|
||||
<string name="theme_light">Claro</string>
|
||||
<string name="theme_dark">Oscuro</string>
|
||||
<string name="theme_system">Sistema</string>
|
||||
<string name="pref_manage_notifications">Gestionar notificaciones</string>
|
||||
<string name="pref_category_security">Seguridad y privacidad</string>
|
||||
<string name="lock_with_biometrics">Requiere desbloqueo</string>
|
||||
@ -369,7 +369,7 @@
|
||||
<string name="gray_background">Gris</string>
|
||||
<string name="pref_true_color_summary">Reduce el efecto anillado en los degradados y mejora la calidad de los grises, pero puede afectar al rendimiento</string>
|
||||
<string name="battery_optimization_setting_activity_not_found">No se pudieron abrir los ajustes del dispositivo</string>
|
||||
<string name="pref_refresh_library_covers">Volver a descargar las portadas en la biblioteca</string>
|
||||
<string name="pref_refresh_library_covers">Actualizar las portadas de la biblioteca</string>
|
||||
<string name="tracking_info">La sincronización de estos servicios solo funciona en un solo sentido. Cada elemento en tu biblioteca tiene un botón de seguimiento y tendrás que configurarlo a mano, uno a uno.</string>
|
||||
<string name="unofficial_extension_message">Esta extensión no es de la lista oficial de extensiones.</string>
|
||||
<string name="ext_unofficial">No oficial</string>
|
||||
@ -383,7 +383,7 @@
|
||||
<string name="action_migrate">Migrar</string>
|
||||
<string name="tabs_header">Pestañas</string>
|
||||
<string name="action_display_show_tabs">Mostrar pestañas de categorías</string>
|
||||
<string name="page_list_empty_error">No parece haber ninguna página</string>
|
||||
<string name="page_list_empty_error">No se encontraron páginas</string>
|
||||
<string name="action_disable_all">Deshabilitar todo</string>
|
||||
<string name="action_enable_all">Habilitar todo</string>
|
||||
<string name="pref_show_reading_mode_summary">Mostrar brevemente el modo actual al abrir el lector</string>
|
||||
@ -458,7 +458,7 @@
|
||||
\nTendrás que instalar las extensiones que falten e iniciar sesión en los servicios de seguimiento para poder usarlos.</string>
|
||||
<string name="update_check_eol">Esta versión de Android ya no es compatible</string>
|
||||
<string name="clipboard_copy_error">No se pudo copiar al portapapeles</string>
|
||||
<string name="pref_dns_over_https">DNS por HTTPS (DoH)</string>
|
||||
<string name="pref_dns_over_https">DNS sobre HTTPS (DoH)</string>
|
||||
<string name="pref_download_new_categories_details">Los elementos de las categorías excluidas no se descargarán, ni siquiera si pertenecen a alguna de las categorías que sí estén incluidas.</string>
|
||||
<string name="pref_category_auto_download">Descarga automática</string>
|
||||
<string name="rotation_landscape">En horizontal</string>
|
||||
@ -476,7 +476,7 @@
|
||||
<string name="include">Incluir: %s</string>
|
||||
<string name="none">Ninguna</string>
|
||||
<string name="pref_library_update_categories_details">Los elementos de las categorías excluidas no se actualizarán, ni siquiera si pertenecen a alguna de las categorías que sí estén incluidas.</string>
|
||||
<string name="action_show_errors">Toca para ver los detalles del error</string>
|
||||
<string name="action_show_errors">Toca para ver los detalles</string>
|
||||
<string name="action_display_show_number_of_items">Mostrar el número de elementos</string>
|
||||
<string name="action_sort_chapter_fetch_date">Fecha de obtención del capítulo</string>
|
||||
<string name="rotation_type">Tipo de rotación</string>
|
||||
@ -554,7 +554,7 @@
|
||||
<string name="download_queue_size_warning">Advertencia: Las descargas grandes pueden llevar a que las fuentes se vuelvan cada vez más lentas y en casos extremos que los servidores limiten o impidan el acceso a Tachiyomi. Toca aquí para más información.</string>
|
||||
<string name="ext_update_all">Actualizar todas</string>
|
||||
<string name="channel_app_updates">Actualizaciones de la aplicación</string>
|
||||
<string name="pref_auto_clear_chapter_cache">Borrar la caché de capítulos al abrir la aplicación</string>
|
||||
<string name="pref_auto_clear_chapter_cache">Limpiar la caché de capítulos al abrir la aplicación</string>
|
||||
<string name="database_clean">Base de datos limpia</string>
|
||||
<string name="clear_database_source_item_count">%1$d entradas que no pertenecen a la biblioteca en la base de datos</string>
|
||||
<string name="extension_api_error">No se pudo descargar el listado de extensiones</string>
|
||||
@ -619,15 +619,15 @@
|
||||
<string name="delete_category">Borrar categoría</string>
|
||||
<string name="delete_category_confirmation">¿Quieres borrar la categoría «%s»\?</string>
|
||||
<string name="internal_error">ErrorInterno: Mira el registro de depuración para más información</string>
|
||||
<string name="pref_user_agent_string">Nombre del navegador a usar («user agent»)</string>
|
||||
<string name="pref_reset_user_agent_string">Restablecer el nombre del navegador («user agent»)</string>
|
||||
<string name="pref_user_agent_string">User agent predeterminado</string>
|
||||
<string name="pref_reset_user_agent_string">Restablecer user agent predeterminado</string>
|
||||
<string name="action_remove_everything">Quitar todo</string>
|
||||
<string name="loader_rar5_error">La app no soporta el formato RARv5</string>
|
||||
<string name="appwidget_updates_description">Aquí aparecerá el contenido más reciente de tu biblioteca</string>
|
||||
<string name="appwidget_unavailable_locked">El widget no está disponible cuando el bloqueo de aplicación está activo</string>
|
||||
<string name="update_already_running">Ya se está actualizando</string>
|
||||
<string name="theme_tidalwave">Marea</string>
|
||||
<string name="error_user_agent_string_blank">La cadena con el agente de usuario no puede estar vacía</string>
|
||||
<string name="error_user_agent_string_blank">El valor del user agent no puede estar en blanco</string>
|
||||
<string name="download_ahead_info">Solo funciona si el capítulo actual y el que va después ya están descargados.</string>
|
||||
<string name="download_ahead">Descargar por adelantado</string>
|
||||
<string name="auto_download_while_reading">Descarga los capítulos siguientes mientras lees</string>
|
||||
@ -654,7 +654,7 @@
|
||||
<string name="unknown_title">Título desconocido</string>
|
||||
<string name="invalid_location">Ubicación incorrecta: %s</string>
|
||||
<string name="updates_last_update_info_just_now">Ahora mismo</string>
|
||||
<string name="error_user_agent_string_invalid">El nombre de agente de usuario no vale</string>
|
||||
<string name="error_user_agent_string_invalid">Valor de user agent inválido</string>
|
||||
<string name="download_notifier_cache_renewal">Reindexando descargas</string>
|
||||
<string name="action_open_random_manga">Abrir un elemento al azar</string>
|
||||
<string name="information_no_entries_found">Parece que esta categoría está vacía</string>
|
||||
@ -768,4 +768,29 @@
|
||||
<string name="action_bar_up_description">Subir un nivel</string>
|
||||
<string name="pref_storage_location">Ubicación del almacenamiento</string>
|
||||
<string name="pref_storage_location_info">Se utiliza para las copias de seguridad automáticas, las descargas de capítulos y la fuente local.</string>
|
||||
<string name="onboarding_storage_action_select">Seleccionar una carpeta</string>
|
||||
<string name="pref_onboarding_guide">Guía de incorporación</string>
|
||||
<string name="onboarding_guides_new_user">¿Nuevo en %s? Recomendamos consultar la guía de introducción.</string>
|
||||
<string name="onboarding_action_finish">Comenzar</string>
|
||||
<string name="onboarding_storage_selection_required">Debe seleccionarse una carpeta</string>
|
||||
<string name="onboarding_heading">Bienvenido!</string>
|
||||
<string name="onboarding_guides_returning_user">¿Ya usaste %s antes?</string>
|
||||
<string name="onboarding_action_skip">Saltar</string>
|
||||
<string name="onboarding_action_next">Siguiente</string>
|
||||
<string name="onboarding_description">Vamos a configurar algunas cosas primero. Siempre puedes volver a cambiarlas más tarde en la configuración.</string>
|
||||
<string name="no_location_set">No se ha establecido una ubicación de almacenamiento</string>
|
||||
<string name="onboarding_storage_info">Selecciona una carpeta donde %1$s almacenará las descargas de capítulos, copias de seguridad y más.
|
||||
\n
|
||||
\nSe recomienda una carpeta dedicada.
|
||||
\n
|
||||
\nCarpeta seleccionada: %2$s</string>
|
||||
<string name="onboarding_permission_install_apps">Permiso para instalar aplicaciones</string>
|
||||
<string name="onboarding_permission_type_optional">Opcional</string>
|
||||
<string name="onboarding_permission_type_required">Requerido</string>
|
||||
<string name="onboarding_permission_notifications">Permiso de notificación</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Evitar interrupciones en las actualizaciones largas de la biblioteca, descargas y restauraciones de copias de seguridad.</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Uso de batería en segundo plano</string>
|
||||
<string name="onboarding_permission_install_apps_description">Para instalar las extensiones de fuentes.</string>
|
||||
<string name="onboarding_permission_notifications_description">Recibe notificaciones sobre actualizaciones de la biblioteca y más.</string>
|
||||
<string name="onboarding_permission_action_grant">Permitir</string>
|
||||
</resources>
|
@ -99,9 +99,9 @@
|
||||
<string name="pref_manage_notifications">Pamahalaan ang mga abiso</string>
|
||||
<string name="pref_category_security">Seguridad at privacy</string>
|
||||
<string name="pref_date_format">Ayos ng petsa</string>
|
||||
<string name="theme_dark">Nakabukas</string>
|
||||
<string name="theme_light">Nakasara</string>
|
||||
<string name="theme_system">Sundan ang sistema</string>
|
||||
<string name="theme_dark">Madilim</string>
|
||||
<string name="theme_light">Maliwanag</string>
|
||||
<string name="theme_system">Sistema</string>
|
||||
<string name="pref_category_about">Patungkol</string>
|
||||
<string name="pref_category_advanced">Karagdagan</string>
|
||||
<string name="pref_category_tracking">Pagta-track</string>
|
||||
@ -115,7 +115,7 @@
|
||||
<string name="last_read_chapter">Huling nabasang kabanata</string>
|
||||
<string name="disabled">Sarado</string>
|
||||
<string name="pref_remove_after_marked_as_read">Pagkamarkahang nabasa na</string>
|
||||
<string name="pref_remove_after_read">Pagkatapos basahin, kusang burahin</string>
|
||||
<string name="pref_remove_after_read">Pagkatapos basahin, awtomatikong burahin</string>
|
||||
<string name="pref_webtoon_side_padding">Kapal ng gilid</string>
|
||||
<string name="pref_category_reading">Pagbabasa</string>
|
||||
<string name="pref_always_show_chapter_transition">Ipakita palagi ang paglipat-kabanata</string>
|
||||
@ -133,7 +133,7 @@
|
||||
<string name="zoom_start_center">Gitna</string>
|
||||
<string name="zoom_start_right">Kanan</string>
|
||||
<string name="zoom_start_left">Kaliwa</string>
|
||||
<string name="zoom_start_automatic">Kusa</string>
|
||||
<string name="zoom_start_automatic">Awtomatiko</string>
|
||||
<string name="pref_zoom_start">Panimulang pag-zoom</string>
|
||||
<string name="scale_type_smart_fit">Matalinong pagsasalaki</string>
|
||||
<string name="scale_type_original_size">Orihinal na laki</string>
|
||||
@ -185,7 +185,7 @@
|
||||
\nMaaaring mabasa ng isang kaduda-dudang extension ang kahit anong nakatagong credentials sa pag-login o di kaya nama\'y magsimula ng delikadong code.
|
||||
\n
|
||||
\nTinatanggap mo ang mga bantang ito sa pagtiwala sa certificate na ito.</string>
|
||||
<string name="untrusted_extension">Kaduda-dudang extension</string>
|
||||
<string name="untrusted_extension">Di-pinagkakatiwalaang extension</string>
|
||||
<string name="ext_uninstall">I-uninstall</string>
|
||||
<string name="ext_trust">Tiwala</string>
|
||||
<string name="ext_untrusted">Kaduda-duda</string>
|
||||
@ -202,17 +202,17 @@
|
||||
<string name="default_category_summary">Palaging tanungin</string>
|
||||
<string name="default_category">Default na kategorya</string>
|
||||
<string name="pref_library_update_refresh_metadata_summary">Maghanap ng mga bagong cover at detalye kapag nag-a-update ng Aklatan</string>
|
||||
<string name="pref_library_update_refresh_metadata">Kusang sariwain ang metadata</string>
|
||||
<string name="pref_library_update_refresh_metadata">Awtomatikong i-refresh ang metadata</string>
|
||||
<string name="pref_update_only_non_completed">May \"Kumpleto\" na estado</string>
|
||||
<string name="charging">Kapag naka-charge</string>
|
||||
<string name="pref_library_update_restriction">Kondisyon sa kusang pag-update</string>
|
||||
<string name="pref_library_update_restriction">Awtomatikong ina-update ang mga paghihigpit sa device</string>
|
||||
<string name="update_weekly">Linggo-linggo</string>
|
||||
<string name="update_48hour">Kada 2 araw</string>
|
||||
<string name="update_24hour">Araw-araw</string>
|
||||
<string name="update_12hour">Kada 12 oras</string>
|
||||
<string name="update_6hour">Kada 6 na oras</string>
|
||||
<string name="update_never">Nakapatay</string>
|
||||
<string name="pref_library_update_interval">Kusang pag-update</string>
|
||||
<string name="pref_library_update_interval">Awtomatikong pag-update</string>
|
||||
<string name="pref_category_library_update">Panlahatang update</string>
|
||||
<string name="landscape">Pahiga</string>
|
||||
<string name="portrait">Patayo</string>
|
||||
@ -385,7 +385,7 @@
|
||||
<string name="pref_disable_battery_optimization_summary">Nakatutulong sa pag-update ng aklatan sa background at pag-backup</string>
|
||||
<string name="pref_refresh_library_covers">I-refresh ang mga cover sa aklatan</string>
|
||||
<string name="clear_database_completed">Binura na</string>
|
||||
<string name="clear_database_confirmation">Sigurado ka ba\? Ang mga nabasang kabanata at pag-unlad ng mga wala sa aklatan ay mawawala</string>
|
||||
<string name="clear_database_confirmation">Sigurado ka ba? Ang mga nabasang kabanata at progress ng mga wala sa aklatan ay mawawala</string>
|
||||
<string name="pref_clear_database_summary">Burahin ang nakaraan ng mga entry na hindi naka-save sa aklatan mo</string>
|
||||
<string name="pref_clear_database">Linisin ang database</string>
|
||||
<string name="cache_delete_error">Nagka-error habang nililinis</string>
|
||||
@ -508,7 +508,7 @@
|
||||
<string name="action_start_downloading_now">I-download na</string>
|
||||
<string name="about_dont_kill_my_app">May dagdag na mga restriksyon sa app ang ilang mga modelo ng phone na pumapatay sa mga serbisyo sa background. May impormasyon sa site na ito para maayos ang naturang problema.</string>
|
||||
<string name="restore_miui_warning">Maaaring hindi gumana nang maayos ang pag-backup/pag-restore kung nakasara ang MIUI optimization.</string>
|
||||
<string name="enhanced_tracking_info">Nagbibigay ng mga pinahusay na mga feature para sa ilang mga source. Kusang tina-track ang mga entry kapag naidagdag ito sa iyong aklatan.</string>
|
||||
<string name="enhanced_tracking_info">Nagbibigay ng mga pinahusay na mga feature para sa ilang mga source. Awtomatikong tina-track ang mga entry kapag naidagdag ito sa iyong aklatan.</string>
|
||||
<string name="enhanced_services">Pinahusay na tracker</string>
|
||||
<string name="theme_midnightdusk">Hatinggabi</string>
|
||||
<string name="theme_greenapple">Berdeng Mansanas</string>
|
||||
@ -519,7 +519,7 @@
|
||||
<string name="theme_yinyang">Yin at Yang</string>
|
||||
<string name="theme_tako">Tako</string>
|
||||
<string name="theme_strawberrydaiquiri">Presas</string>
|
||||
<string name="label_background_activity">Gawaing likuran</string>
|
||||
<string name="label_background_activity">Aktibidad sa background</string>
|
||||
<string name="pref_lowest">Pinakamababa</string>
|
||||
<string name="pref_low">Mababa</string>
|
||||
<string name="pref_high">Mataas</string>
|
||||
@ -535,9 +535,9 @@
|
||||
<string name="getting_started_guide">Gabay sa Pagsisimula</string>
|
||||
<string name="pref_tablet_ui_mode">Pang-tablet na UI</string>
|
||||
<string name="help_translate">Tumulong sa pagsalin</string>
|
||||
<string name="pref_remove_exclude_categories">Kategoryang di-kasama</string>
|
||||
<string name="pref_remove_exclude_categories">Mga hindi kasamang kategorya</string>
|
||||
<string name="ext_app_info">Tungkol sa app</string>
|
||||
<string name="ext_installer_shizuku_unavailable_dialog">Paki-install at buksan ang Shizuku para magamit ito bilang taga-install ng extension.</string>
|
||||
<string name="ext_installer_shizuku_unavailable_dialog">I-Install at buksan ang Shizuku para magamit ito bilang taga-install ng extension.</string>
|
||||
<string name="ext_installer_shizuku_stopped">Di tumatakbo ang Shizuku</string>
|
||||
<string name="ext_installer_legacy">Legasiya</string>
|
||||
<string name="ext_installer_pref">Taga-install</string>
|
||||
@ -551,7 +551,7 @@
|
||||
<string name="backup_info">Dapat nagtatabi rin kayo ng mga kopya ng backup sa ibang mga lugar. Ang mga backup ay naglalaman ng sensitibong data tulad ng nakaimbak na password; mag-ingat kung ibahagi ito.</string>
|
||||
<string name="connected_to_wifi">Sa Wi-Fi lang</string>
|
||||
<string name="update_72hour">Kada 3 araw</string>
|
||||
<string name="download_queue_size_warning">Babala: maaaring humantong sa pagbagal at/o pagharang ng mga source sa Tachiyomi ang mga malalaking maramihang pag-download. I-tap para matuto pa.</string>
|
||||
<string name="download_queue_size_warning">Babala: maaaring humantong sa pagbagal at/o pagharang ng mga source sa Tachiyomi ang maramihang pag-download. I-tap para matuto pa.</string>
|
||||
<string name="channel_app_updates">Mga update sa app</string>
|
||||
<string name="ext_update_all">I-update lahat</string>
|
||||
<string name="clear_database_source_item_count">%1$d na entry sa database na wala sa aklatan</string>
|
||||
@ -573,7 +573,7 @@
|
||||
<string name="pref_update_only_started">Hindi pa nasisimulan</string>
|
||||
<string name="skipped_reason_not_caught_up">Nilaktawan dahil may di pa nabasang mga kabanata</string>
|
||||
<string name="skipped_reason_not_started">Nilaktawan dahil wala pang nabasang mga kabanata</string>
|
||||
<string name="pref_landscape_zoom">Kusang mag-zoom sa mga malalawak na larawan</string>
|
||||
<string name="pref_landscape_zoom">Awtomatikong mag-zoom sa mga malalawak na larawan</string>
|
||||
<string name="pref_navigate_pan">I-pan ang mga malalapad na larawan</string>
|
||||
<string name="learn_more">Matuto pa</string>
|
||||
<string name="channel_skipped">Nilaktawan</string>
|
||||
@ -640,7 +640,7 @@
|
||||
<string name="skipped_reason_not_always_update">Nilaktawan dahil hindi kailangan ang pag-update sa serye</string>
|
||||
<string name="action_search_hint">Maghanap…</string>
|
||||
<string name="pref_reader_summary">Paraan ng pagbasa, pagpapakita, nabigasyon</string>
|
||||
<string name="pref_downloads_summary">Kusang pag-download, i-download agad</string>
|
||||
<string name="pref_downloads_summary">Awtomatikong pag-download, i-download nang maaga</string>
|
||||
<string name="pref_tracking_summary">Isahang pagsabay sa progress, pinahusay na pagsabay</string>
|
||||
<string name="pref_appearance_summary">Tema, ayos ng petsa & oras</string>
|
||||
<string name="pref_backup_summary">Mano-mano at awtomatikong pag-backup, espasyo sa storage</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="selected">Napili</string>
|
||||
<string name="not_selected">Di napili</string>
|
||||
<string name="action_bar_up_description">Mag-navigate pataas</string>
|
||||
<string name="onboarding_storage_action_select">Pumili ng folder</string>
|
||||
<string name="pref_onboarding_guide">Gabay sa onboarding</string>
|
||||
<string name="onboarding_guides_new_user">Bago sa %s? Inirerekomenda naming tingnan ang gabay sa pagsisimula.</string>
|
||||
<string name="onboarding_action_finish">Magsimula</string>
|
||||
<string name="onboarding_storage_selection_required">Dapat pumili ng isang folder</string>
|
||||
<string name="onboarding_heading">Maligayang pagdating!</string>
|
||||
<string name="onboarding_guides_returning_user">Gumagamit ba ng %s dati?</string>
|
||||
<string name="onboarding_action_skip">Laktawan</string>
|
||||
<string name="onboarding_action_next">Susunod</string>
|
||||
<string name="onboarding_description">Mag-set up muna tayo ng ilang bagay. Maaari mo ring baguhin ang mga ito anumang oras sa mga setting sa ibang pagkakataon.</string>
|
||||
<string name="no_location_set">Walang nakatakdang lokasyon ng storage</string>
|
||||
<string name="onboarding_storage_info">Pumili ng folder kung saan mag-imbak ang %1$s ng mga na-download ng kabanata, mga backup, at higit pa.
|
||||
\n
|
||||
\nInirerekomenda ang isang nakalaang folder.
|
||||
\n
|
||||
\nNapiling folder: %2$s</string>
|
||||
</resources>
|
@ -296,9 +296,9 @@
|
||||
<string name="action_sort_latest_chapter">最新章の更新順</string>
|
||||
<string name="action_view_chapters">章を見る</string>
|
||||
<string name="action_cancel_all">すべてキャンセル</string>
|
||||
<string name="theme_light">オフ</string>
|
||||
<string name="theme_dark">オン</string>
|
||||
<string name="theme_system">システムに従う</string>
|
||||
<string name="theme_light">ライト</string>
|
||||
<string name="theme_dark">ダーク</string>
|
||||
<string name="theme_system">システム</string>
|
||||
<string name="pref_manage_notifications">通知設定</string>
|
||||
<string name="pref_category_security">セキュリティとプライバシー</string>
|
||||
<string name="lock_with_biometrics">アンロックを必要とする</string>
|
||||
@ -768,4 +768,29 @@
|
||||
<string name="action_bar_up_description">上に移動</string>
|
||||
<string name="pref_storage_location">保存場所</string>
|
||||
<string name="pref_storage_location_info">自動バックアップ、章のダウンロード、ローカル ソースの保存位置となります。</string>
|
||||
<string name="onboarding_storage_action_select">フォルダを選択してください</string>
|
||||
<string name="pref_onboarding_guide">初回設定ガイド</string>
|
||||
<string name="onboarding_guides_new_user">%sは初めて?入門ガイドをチェックしてみしましょう。</string>
|
||||
<string name="onboarding_action_finish">はじめる</string>
|
||||
<string name="onboarding_storage_selection_required">フォルダを選択してください</string>
|
||||
<string name="onboarding_heading">ようこそ!</string>
|
||||
<string name="onboarding_guides_returning_user">%sを使ったことはもうありましたか?</string>
|
||||
<string name="onboarding_action_skip">スキップ</string>
|
||||
<string name="onboarding_action_next">次へ</string>
|
||||
<string name="onboarding_description">はじめに初回設定をしていきましょう。このあとも「設定」にていつも変更できます。</string>
|
||||
<string name="no_location_set">保存場所が設定されていません</string>
|
||||
<string name="onboarding_storage_info">%1$sのダウンロード、バックアップなどの保存先のフォルダを設定してください。
|
||||
\n
|
||||
\nアプリ専用のフォルダの作成・使用がおすすめです。
|
||||
\n
|
||||
\n選択したフォルダ:%2$s</string>
|
||||
<string name="onboarding_permission_notifications">通知の許可</string>
|
||||
<string name="onboarding_permission_install_apps">アプリのインストールの許可</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">時間のかかるライブラリ更新、ダウンロードやバックアップの復元などへの中断を防ぎます。</string>
|
||||
<string name="onboarding_permission_type_optional">任意</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">バックグラウンドでのバッテリー使用量</string>
|
||||
<string name="onboarding_permission_install_apps_description">ソース拡張機能をインストールするために必要です。</string>
|
||||
<string name="onboarding_permission_notifications_description">ライブラリ更新などの通知を送信します。</string>
|
||||
<string name="onboarding_permission_type_required">必須</string>
|
||||
<string name="onboarding_permission_action_grant">許可</string>
|
||||
</resources>
|
@ -150,7 +150,7 @@
|
||||
<string name="cookies_cleared">쿠키 삭제됨</string>
|
||||
<string name="pref_clear_database">데이터베이스 삭제</string>
|
||||
<string name="pref_clear_database_summary">서재에 추가되지 않은 항목의 기록을 삭제합니다</string>
|
||||
<string name="clear_database_confirmation">확실합니까\? 서재에 없는 항목의 읽은 기록이 삭제됩니다</string>
|
||||
<string name="clear_database_confirmation">확실합니까? 서재에 없는 항목의 읽기 기록이 삭제됩니다</string>
|
||||
<string name="version">버전</string>
|
||||
<string name="pref_enable_acra">오류 보고서 전송</string>
|
||||
<string name="pref_acra_summary">버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다</string>
|
||||
@ -309,7 +309,7 @@
|
||||
<string name="pref_disable_battery_optimization">배터리 최적화 끄기</string>
|
||||
<string name="restore_miui_warning">MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다.</string>
|
||||
<string name="restore_in_progress">복원이 이미 진행중 입니다</string>
|
||||
<string name="requires_app_restart">설정을 적용하기 위해 앱을 재시작해야 합니다</string>
|
||||
<string name="requires_app_restart">앱을 재시작한 후에 적용됩니다</string>
|
||||
<string name="pref_dns_over_https">DNS over HTTPS (DoH)</string>
|
||||
<string name="label_data">데이터</string>
|
||||
<string name="backup_in_progress">백업이 이미 진행중입니다</string>
|
||||
@ -448,7 +448,7 @@
|
||||
<string name="backup_restore_missing_sources">없어진 소스:</string>
|
||||
<string name="backup_restore_missing_trackers">로그인 되지않은 트래커:</string>
|
||||
<string name="pref_auto_clear_chapter_cache">앱 실행 시 회차 캐시 삭제</string>
|
||||
<string name="clear_database_source_item_count">데이터베이스에 없는 항목이 %1$d개 있습니다</string>
|
||||
<string name="clear_database_source_item_count">서재에 없는 항목이 데이터베이스에 %1$d개 있습니다</string>
|
||||
<string name="about_dont_kill_my_app">일부 제조사는 백그라운드 서비스를 종료하는 추가적인 제한 사항이 있습니다. 자세한 사항은 웹사이트를 참조하세요.</string>
|
||||
<string name="pref_tablet_ui_mode">태블릿 UI</string>
|
||||
<string name="tabs_header">탭</string>
|
||||
@ -622,7 +622,7 @@
|
||||
<string name="cant_open_last_read_chapter">마지막 회차를 열 수 없습니다</string>
|
||||
<string name="appwidget_updates_description">최근에 업데이트된 항목 보기</string>
|
||||
<string name="on_hold_list">보류 목록</string>
|
||||
<string name="download_notifier_split_page_not_found">분할하는 동안 %d 페이지를 찾을 수 없습니다</string>
|
||||
<string name="download_notifier_split_page_not_found">분리 중 페이지 %d을 찾을 수 없습니다</string>
|
||||
<string name="loader_rar5_error">RARv5 포맷은 지원되지 않습니다</string>
|
||||
<string name="appwidget_unavailable_locked">앱 잠금 사용 중에는 위젯을 이용할 수 없습니다</string>
|
||||
<string name="theme_tidalwave">파도</string>
|
||||
|
@ -300,8 +300,8 @@
|
||||
<string name="label_more">Mer</string>
|
||||
<string name="action_view_chapters">Vis kapitler</string>
|
||||
<string name="action_cancel_all">Avbryt alle</string>
|
||||
<string name="theme_light">Av</string>
|
||||
<string name="theme_dark">På</string>
|
||||
<string name="theme_light">Lyst</string>
|
||||
<string name="theme_dark">Mørkt</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="pref_manage_notifications">Håndter merknader</string>
|
||||
<string name="pref_category_security">Sikkerhet og personvern</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="label_data_storage">Data og lagring</string>
|
||||
<string name="file_null_uri_error">Ingen fil valgt</string>
|
||||
<string name="exclude_scanlators">Ekskluder skanningsoversettere</string>
|
||||
<string name="onboarding_storage_action_select">Velg en mappe</string>
|
||||
<string name="pref_onboarding_guide">Introduksjonsguide</string>
|
||||
<string name="onboarding_guides_new_user">Ny til %s? Vi anbefaler å sjekke ut startveiledningen.</string>
|
||||
<string name="onboarding_action_finish">Kom i gang</string>
|
||||
<string name="onboarding_storage_selection_required">En mappe må velges</string>
|
||||
<string name="onboarding_heading">Velkommen!</string>
|
||||
<string name="onboarding_guides_returning_user">Allerede brukt %s før?</string>
|
||||
<string name="onboarding_action_skip">Hopp over</string>
|
||||
<string name="onboarding_action_next">Neste</string>
|
||||
<string name="onboarding_description">La oss sette opp noen ting først. Du kan alltid endre disse i innstillingene senere også.</string>
|
||||
<string name="no_location_set">Ingen lagringsplassering angitt</string>
|
||||
<string name="onboarding_storage_info">Velg en mappe der %1$s vil lagre kapittelnedlastinger, sikkerhetskopier og mer.
|
||||
\n
|
||||
\nEn dedikert mappe anbefales.
|
||||
\n
|
||||
\nValgt mappe: %2$s</string>
|
||||
</resources>
|
@ -783,4 +783,14 @@
|
||||
\nUma pasta dedicada é recomendada.
|
||||
\n
|
||||
\nPasta selecionada: %2$s</string>
|
||||
<string name="onboarding_storage_selection_required">Uma pasta deve ser selecionada</string>
|
||||
<string name="onboarding_permission_notifications">Permissão de notificação</string>
|
||||
<string name="onboarding_permission_install_apps">Permissão de instalação de aplicativos</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Evite interrupções para tarefas longas como atualizações da biblioteca, downloads e restauração de backups.</string>
|
||||
<string name="onboarding_permission_type_optional">Opcional</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Uso de bateria em plano de fundo</string>
|
||||
<string name="onboarding_permission_install_apps_description">Para instalar extensões de fontes.</string>
|
||||
<string name="onboarding_permission_notifications_description">Seja notificado para atualizações da biblioteca e mais.</string>
|
||||
<string name="onboarding_permission_type_required">Obrigatório</string>
|
||||
<string name="onboarding_permission_action_grant">Conceder</string>
|
||||
</resources>
|
@ -301,8 +301,8 @@
|
||||
<string name="action_sort_latest_chapter">Последняя глава</string>
|
||||
<string name="action_view_chapters">Просмотреть главы</string>
|
||||
<string name="action_cancel_all">Отменить всё</string>
|
||||
<string name="theme_light">Выключен</string>
|
||||
<string name="theme_dark">Включён</string>
|
||||
<string name="theme_light">Светлая</string>
|
||||
<string name="theme_dark">Тёмная</string>
|
||||
<string name="theme_system">Система</string>
|
||||
<string name="pref_manage_notifications">Управление уведомлениями</string>
|
||||
<string name="pref_category_security">Безопасность и конфиденциальность</string>
|
||||
@ -768,4 +768,20 @@
|
||||
<string name="action_bar_up_description">Перейти вверх</string>
|
||||
<string name="pref_storage_location">Путь хранилища</string>
|
||||
<string name="pref_storage_location_info">Используется для автоматических резевных копии, загрузок глав и источнике на устройстве.</string>
|
||||
<string name="onboarding_storage_action_select">Выбрать папку</string>
|
||||
<string name="pref_onboarding_guide">Руководство для начинающих</string>
|
||||
<string name="onboarding_guides_new_user">Новичок в %s? Мы настоятельно рекомендуем ознакомиться с нашим руководством.</string>
|
||||
<string name="onboarding_action_finish">Начать</string>
|
||||
<string name="onboarding_storage_selection_required">Необходимо выбрать папку</string>
|
||||
<string name="onboarding_heading">Добро пожаловать!</string>
|
||||
<string name="onboarding_guides_returning_user">Уже использовали %s раньше?</string>
|
||||
<string name="onboarding_action_skip">Пропустить</string>
|
||||
<string name="onboarding_action_next">Следующее</string>
|
||||
<string name="onboarding_description">Давайте настроем парочку вещей. Вы всегда можете их поменять позже в настройках.</string>
|
||||
<string name="no_location_set">Не указан путь хранилища</string>
|
||||
<string name="onboarding_storage_info">Выберите папку где %1$s будет хранить загруженные главы, резервные копии и другое.
|
||||
\n
|
||||
\nРекомендуется использовать выделенную папку.
|
||||
\n
|
||||
\nВыбранная папка: %2$s</string>
|
||||
</resources>
|
@ -26,12 +26,12 @@
|
||||
<string name="action_webview_refresh">Rifresko</string>
|
||||
<string name="app_not_available">Aplikacioni i padisponueshem</string>
|
||||
<string name="pref_downloads_summary">Shkarkim automatik, shkarko përpara</string>
|
||||
<string name="theme_dark">Aktiv</string>
|
||||
<string name="theme_dark">Errët</string>
|
||||
<string name="theme_greenapple">Molle jeshile</string>
|
||||
<string name="theme_lavender">Livando</string>
|
||||
<string name="theme_yinyang">Yin & Yang</string>
|
||||
<string name="theme_yotsuba">Yotsuba</string>
|
||||
<string name="theme_tidalwave">valët e baticës</string>
|
||||
<string name="theme_tidalwave">Valët e Baticës</string>
|
||||
<string name="pref_dark_theme_pure_black">E zezë e pastër modaliteti i errët</string>
|
||||
<string name="pref_manage_notifications">Menaxho njoftimet</string>
|
||||
<string name="pref_app_language">Gjuha e aplikacionit</string>
|
||||
@ -46,7 +46,7 @@
|
||||
<string name="pref_show_nsfw_source">Shfaq në listat e burimeve dhe shtesave</string>
|
||||
<string name="relative_time_today">Sot</string>
|
||||
<string name="pref_category_display">Shfaqja</string>
|
||||
<string name="pref_library_columns">Artikuj për rresht</string>
|
||||
<string name="pref_library_columns">Përmasat e grafikut</string>
|
||||
<string name="pref_category_library_update">Përditësim global</string>
|
||||
<string name="update_never">Joaktiv</string>
|
||||
<string name="update_12hour">Çdo 12 orë</string>
|
||||
@ -55,11 +55,11 @@
|
||||
<string name="connected_to_wifi">Vetëm në Wi-Fi</string>
|
||||
<string name="network_not_metered">Vetëm në rrjet pa matje</string>
|
||||
<string name="charging">Gjatë karikimit</string>
|
||||
<string name="pref_library_update_manga_restriction">Kapërceni përditësimin e hyrjeve</string>
|
||||
<string name="pref_library_update_manga_restriction">Kapërceni përditësimin e elementeve</string>
|
||||
<string name="pref_update_only_completely_read">Me kapituj të palexuar</string>
|
||||
<string name="pref_library_update_refresh_metadata_summary">Kontrolloni për kopertinë dhe detaje të reja kur përditësoni bibliotekën</string>
|
||||
<string name="default_category">Kategoria e parazgjedhur</string>
|
||||
<string name="pref_library_update_categories_details">Regjistrimet në kategoritë e përjashtuara nuk do të përditësohen edhe nëse janë gjithashtu në kategoritë e përfshira.</string>
|
||||
<string name="pref_library_update_categories_details">Elementet në kategoritë e përjashtuara nuk do të përditësohen edhe nëse janë gjithashtu në kategoritë e përfshira.</string>
|
||||
<string name="all">Të gjitha</string>
|
||||
<string name="none">Asnje</string>
|
||||
<string name="include">Përfshi: %s</string>
|
||||
@ -74,7 +74,7 @@
|
||||
<string name="download_notifier_text_only_wifi">Nuk ofrohet lidhje Wi-Fi</string>
|
||||
<string name="information_empty_library">Biblioteka juaj është bosh</string>
|
||||
<string name="getting_started_guide">Udhëzues për fillimin</string>
|
||||
<string name="information_no_entries_found">Nuk u gjet asnjë hyrje në këtë kategori</string>
|
||||
<string name="information_no_entries_found">Nuk u gjet asnjë element në këtë kategori</string>
|
||||
<string name="information_empty_category">Nuk ke kategori. Prekni butonin plus për të krijuar një për organizimin e bibliotekës tuaj.</string>
|
||||
<string name="information_webview_required">WebView kërkohet për Tachiyomi</string>
|
||||
<string name="information_cloudflare_bypass_failure">Dështoi të anashkalojë Cloudflare</string>
|
||||
@ -96,18 +96,18 @@
|
||||
<string name="download_notifier_downloader_title">Shkarkues</string>
|
||||
<string name="information_webview_outdated">Përditësoni aplikacionin WebView për përputhshmëri më të mirë</string>
|
||||
<string name="label_default">E paracaktuar</string>
|
||||
<string name="action_filter_tracked">Gjurmuar</string>
|
||||
<string name="action_filter_tracked">I gjurmuar</string>
|
||||
<string name="action_settings">Cilësimet</string>
|
||||
<string name="action_filter_unread">e palexuar</string>
|
||||
<string name="action_filter_unread">E palexuar</string>
|
||||
<string name="action_filter_empty">Hiq filtrin</string>
|
||||
<string name="action_sort_alpha">Sipas alfabetit</string>
|
||||
<string name="action_sort_total">Totali i kapitujve</string>
|
||||
<string name="label_warning">Paralajmërim</string>
|
||||
<string name="action_sort_count">Totali i hyrjeve</string>
|
||||
<string name="action_sort_count">Totali i elementeve</string>
|
||||
<string name="action_sort_last_read">Leximi i fundit</string>
|
||||
<string name="confirm_lock_change">Vërtetoni për të konfirmuar ndryshimin</string>
|
||||
<string name="confirm_lock_change">Vërtetohuni për të konfirmuar ndryshimin</string>
|
||||
<string name="action_menu">Menuja</string>
|
||||
<string name="action_filter">Filtro</string>
|
||||
<string name="action_filter">Filter</string>
|
||||
<string name="action_order_by_chapter_number">Sipas numrit të kapitullit</string>
|
||||
<string name="action_display_download_badge">Kapitujt e shkarkuar</string>
|
||||
<string name="action_display_local_badge">Burimi lokal</string>
|
||||
@ -148,7 +148,7 @@
|
||||
<string name="pref_category_library">Biblioteka</string>
|
||||
<string name="pref_category_reader">Lexues</string>
|
||||
<string name="pref_category_downloads">Shkarkimet</string>
|
||||
<string name="pref_category_tracking">Ndjekja</string>
|
||||
<string name="pref_category_tracking">Gjurmimi</string>
|
||||
<string name="pref_browse_summary">Burimet, zgjerimet, kërkimi global</string>
|
||||
<string name="pref_backup_summary">Rezervime manuale & automatike</string>
|
||||
<string name="pref_category_advanced">E avancuar</string>
|
||||
@ -158,8 +158,8 @@
|
||||
<string name="pref_category_about">Rreth</string>
|
||||
<string name="pref_advanced_summary">Hidh regjistrat e përplasjeve, optimizimet e baterisë</string>
|
||||
<string name="pref_category_theme">Tema</string>
|
||||
<string name="theme_system">Ndiq sistemin</string>
|
||||
<string name="theme_light">Joaktiv</string>
|
||||
<string name="theme_system">Ndiq Sistemin</string>
|
||||
<string name="theme_light">Ndrçim</string>
|
||||
<string name="theme_strawberrydaiquiri">Daiquiri luleshtrydhe</string>
|
||||
<string name="pref_app_theme">Tema e aplikacionit</string>
|
||||
<string name="theme_monet">Dinamik</string>
|
||||
@ -184,11 +184,11 @@
|
||||
<string name="ext_update_all">Përditëso të gjitha</string>
|
||||
<string name="ext_updates_pending">Përditësimet në pritje</string>
|
||||
<string name="on">Aktiv</string>
|
||||
<string name="off">fikur</string>
|
||||
<string name="off">Fikur</string>
|
||||
<string name="categories">Kategoritë</string>
|
||||
<string name="manga">Regjistrimet e bibliotekës</string>
|
||||
<string name="manga">Elementet e bibliotekës</string>
|
||||
<string name="chapters">Kapituj</string>
|
||||
<string name="track">Ndjekja</string>
|
||||
<string name="track">Gjurmimi</string>
|
||||
<string name="history">Historia</string>
|
||||
<string name="action_sort_last_manga_update">Kontrolli i përditësimit të fundit</string>
|
||||
<string name="action_sort_unread_count">Numër i palexuar</string>
|
||||
@ -237,7 +237,7 @@
|
||||
<string name="action_display">Ekrani</string>
|
||||
<string name="action_display_mode">Modaliteti i ekranit</string>
|
||||
<string name="action_display_grid">Rrjetë kompakte</string>
|
||||
<string name="appwidget_updates_description">Shiko përditësimin e fundit te bibliotekes tuaj</string>
|
||||
<string name="appwidget_updates_description">Shiko përditësimin e fundit të elementeve të bibliotekes tuaj</string>
|
||||
<string name="remove_manga">Ju jeni gati të hiqni \"%s\" nga biblioteka juaj</string>
|
||||
<string name="appwidget_unavailable_locked">Miniaplikacioni nuk ofrohet kur kyçja e aplikacionit është aktivizuar</string>
|
||||
<string name="ext_pending">Në pritje</string>
|
||||
@ -351,9 +351,9 @@
|
||||
<string name="auto_download_while_reading">Shkarkim automatik gjatë leximit</string>
|
||||
<string name="save_chapter_as_cbz">Ruaje si arkiv CBZ</string>
|
||||
<string name="tracking_guide">Udhëzues gjurmimi</string>
|
||||
<string name="enhanced_services">Shërbime të përmirësuara</string>
|
||||
<string name="enhanced_services">Gjurmues të përmirësuara</string>
|
||||
<string name="transition_no_next">Nuk ka kapitull tjetër</string>
|
||||
<string name="pref_create_folder_per_manga_summary">Krijon dosje sipas titullit të hyrjeve</string>
|
||||
<string name="pref_create_folder_per_manga_summary">Krijon dosje sipas titullit të elementeve</string>
|
||||
<string name="black_background">E zezë</string>
|
||||
<string name="pref_viewer_type">Modaliteti i parazgjedhur i leximit</string>
|
||||
<string name="l_nav">Në formë L</string>
|
||||
@ -385,20 +385,20 @@
|
||||
<string name="second_to_last">Kapitulli i dytë deri tek i fundit i lexuar</string>
|
||||
<string name="third_to_last">Kapitulli i tretë deri tek i fundit i lexuar</string>
|
||||
<string name="fifth_to_last">Kapitulli i pestë deri tek i fundit i lexuar</string>
|
||||
<string name="pref_download_new_categories_details">Regjistrimet në kategoritë e përjashtuara nuk do të shkarkohen edhe nëse janë gjithashtu në kategoritë e përfshira.</string>
|
||||
<string name="pref_download_new_categories_details">Elementet në kategoritë e përjashtuara nuk do të shkarkohen edhe nëse janë gjithashtu në kategoritë e përfshira.</string>
|
||||
<string name="download_ahead_info">Punon vetëm në hyrjet në bibliotekë dhe nëse kapitulli aktual plus kapitulli tjetër janë shkarkuar tashmë</string>
|
||||
<string name="services">Shërbimet</string>
|
||||
<string name="services">Gjurmuesët</string>
|
||||
<string name="split_tall_images_summary">Përmirëson performancën e lexuesit</string>
|
||||
<string name="pref_auto_update_manga_sync">Përditëso progresin pas leximit</string>
|
||||
<string name="tracking_info">Sinkronizimi i njëanshëm për të përditësuar përparimin e kapitullit në shërbimet e gjurmimit. Konfiguro gjurmimin për hyrjet individuale nga butoni i tyre i gjurmimit.</string>
|
||||
<string name="action_filter_bookmarked">faqeshënuar</string>
|
||||
<string name="tracking_info">Sinkronizimi i njëanshëm për të përditësuar përparimin e kapitullit në shërbimet e gjurmimit. Konfiguro gjurmimin për elementet individuale nga butoni i tyre i gjurmimit.</string>
|
||||
<string name="action_filter_bookmarked">Faqeshënuar</string>
|
||||
<string name="webtoon_viewer">Webtoon</string>
|
||||
<string name="disabled">I çaktivizuar</string>
|
||||
<string name="kindlish_nav">Kindle-ish</string>
|
||||
<string name="enhanced_tracking_info">Shërbime që ofrojnë veçori të përmirësuara për burime specifike. Regjistrimet gjurmohen automatikisht kur shtohen në bibliotekën tuaj.</string>
|
||||
<string name="enhanced_tracking_info">Ofron tipare të përmirësuara për burime specifike. Elementet gjurmohen automatikisht kur shtohen në bibliotekën tuaj.</string>
|
||||
<string name="action_track">Pista</string>
|
||||
<string name="pref_backup_interval">Frekuenca rezervë</string>
|
||||
<string name="backup_restore_missing_trackers">Gjurmuesit nuk kanë hyrë në:</string>
|
||||
<string name="backup_restore_missing_trackers">I pa identifikuar ne gjurmuesit:</string>
|
||||
<string name="restore_duration">%02d min, %02d sek</string>
|
||||
<string name="restore_miui_warning">Rezervimi/rivendosja mund të mos funksionojë siç duhet nëse Optimizimi MIUI është i çaktivizuar.</string>
|
||||
<string name="pref_reset_user_agent_string">Rivendos vargun e parazgjedhur të agjentit të përdoruesit</string>
|
||||
@ -416,7 +416,7 @@
|
||||
<string name="sort_by_upload_date">Sipas datës së ngarkimit</string>
|
||||
<string name="manga_download">Shkarko</string>
|
||||
<string name="unread">Të palexuara</string>
|
||||
<string name="manga_tracking_tab">Ndjekja</string>
|
||||
<string name="manga_tracking_tab">Gjurmimi</string>
|
||||
<string name="unfinished_list">Lista e papërfunduar</string>
|
||||
<string name="on_hold_list">Në listën e pritjes</string>
|
||||
<string name="track_type">Lloji</string>
|
||||
@ -444,7 +444,7 @@
|
||||
<string name="migration_dialog_what_to_include">Zgjidhni të dhënat për të përfshirë</string>
|
||||
<string name="download_queue_error">Kapitujt nuk mund të shkarkoheshin. Mund të provoni përsëri në seksionin e shkarkimeve</string>
|
||||
<string name="notification_size_warning">Përditësimet e mëdha dëmtojnë burimet dhe mund të çojnë në përditësime më të ngadalta dhe gjithashtu rritje të përdorimit të baterisë. Trokit për të mësuar më shumë.</string>
|
||||
<string name="clear_database_completed">Hyrjet u fshinë</string>
|
||||
<string name="clear_database_completed">Elementet u fshinë</string>
|
||||
<string name="pref_disable_battery_optimization">Çaktivizo optimizimin e baterisë</string>
|
||||
<string name="email">Adresa e emailit</string>
|
||||
<string name="login_title">Hyni në %1$s</string>
|
||||
@ -459,7 +459,7 @@
|
||||
<string name="pref_invalidate_download_cache">Anuloni indeksin e shkarkimeve</string>
|
||||
<string name="pref_auto_clear_chapter_cache">Pastro memorjen e kapitullit në mbylljen e aplikacionit</string>
|
||||
<string name="pref_clear_database">Pastro bazën e të dhënave</string>
|
||||
<string name="clear_database_confirmation">A je i sigurt\? Lexoni kapitujt dhe përparimi i hyrjeve që nuk janë në bibliotekë do të humbasin</string>
|
||||
<string name="clear_database_confirmation">A je i sigurt? Kapitujt e lexuar dhe progresi i elementeve që nuk janë në bibliotekë do të humbasin</string>
|
||||
<string name="no_pinned_sources">Nuk ke burime të gozhduara</string>
|
||||
<string name="local_filter_order_by">Urdhër nga</string>
|
||||
<string name="date">Data</string>
|
||||
@ -506,7 +506,7 @@
|
||||
<string name="creating_backup">Duke krijuar rezervë</string>
|
||||
<string name="creating_backup_error">Rezervimi dështoi</string>
|
||||
<string name="missing_storage_permission">Lejet e ruajtjes nuk janë dhënë</string>
|
||||
<string name="empty_backup_error">Nuk ka hyrje në bibliotekë për të rezervuar</string>
|
||||
<string name="empty_backup_error">Nuk ka element në bibliotekë për të rezervuar</string>
|
||||
<string name="restore_in_progress">Rivendosja është tashmë në proces</string>
|
||||
<string name="restoring_backup">Rivendosja e rezervës</string>
|
||||
<string name="restoring_backup_error">Rivendosja e rezervimit dështoi</string>
|
||||
@ -522,7 +522,7 @@
|
||||
<string name="label_data">Të dhënat</string>
|
||||
<string name="used_cache">Përdorur: %1$s</string>
|
||||
<string name="cache_deleted">Memoria e fshehtë u pastrua. %1$d skedarë janë fshirë</string>
|
||||
<string name="clear_database_source_item_count">%1$d hyrje jashtë bibliotekës në bazën e të dhënave</string>
|
||||
<string name="clear_database_source_item_count">%1$d element jashtë bibliotekës në bazën e të dhënave</string>
|
||||
<string name="pref_clear_webview_data">Pastro të dhënat e WebView</string>
|
||||
<string name="pref_refresh_library_covers">Rifresko kopertinat e bibliotekës</string>
|
||||
<string name="pref_reset_viewer_flags">Rivendos cilësimet e lexuesit për seri</string>
|
||||
@ -543,7 +543,7 @@
|
||||
<string name="label_downloaded_only">Vetëm të shkarkuarat</string>
|
||||
<string name="pref_incognito_mode">Modaliteti i fshehtë</string>
|
||||
<string name="pref_incognito_mode_summary">Ndalon leximin e historisë</string>
|
||||
<string name="downloaded_only_summary">Filtro të gjitha hyrjet në bibliotekën tuaj</string>
|
||||
<string name="downloaded_only_summary">Filtro të gjitha elementet në bibliotekën tuaj</string>
|
||||
<string name="logout_title">Dil nga %1$s\?</string>
|
||||
<string name="logout_success">Tani keni dalë nga llogaria</string>
|
||||
<string name="unknown_error">Gabim i panjohur</string>
|
||||
@ -578,7 +578,7 @@
|
||||
<string name="cover_saved">Kopertina u ruajt</string>
|
||||
<string name="error_sharing_cover">Gabim në ndarjen e kopertinës</string>
|
||||
<string name="confirm_delete_chapters">Jeni i sigurt që dëshironi të fshini kapitujt e zgjedhur\?</string>
|
||||
<string name="also_set_chapter_settings_for_library">Aplikoni gjithashtu për të gjitha hyrjet në bibliotekën time</string>
|
||||
<string name="also_set_chapter_settings_for_library">Aplikoni gjithashtu për të gjitha elementet në bibliotekën time</string>
|
||||
<string name="set_chapter_settings_as_default">Vendose si parësore</string>
|
||||
<string name="no_chapters_error">Nuk u gjet asnjë kapitull</string>
|
||||
<string name="are_you_sure">A je i sigurt\?</string>
|
||||
@ -597,7 +597,7 @@
|
||||
<string name="track_started_reading_date">Data e fillimit</string>
|
||||
<string name="track_status">Status</string>
|
||||
<string name="track_finished_reading_date">Data e mbarimit</string>
|
||||
<string name="track_remove_date_conf_title">Të hiqet data\?</string>
|
||||
<string name="track_remove_date_conf_title">Hiqni datën?</string>
|
||||
<string name="track_remove_finish_date_conf_text">Kjo do të heqë datën e përfundimit të zgjedhur më parë nga %s</string>
|
||||
<string name="snack_categories_deleted">Kategoritë u fshinë</string>
|
||||
<string name="picture_saved">Fotografia u ruajt</string>
|
||||
@ -641,10 +641,10 @@
|
||||
\n
|
||||
\n Do t\'ju duhet të instaloni çdo shtesë që mungon dhe më pas të identifikoheni në shërbimet e gjurmimit për t\'i përdorur ato.</string>
|
||||
<string name="invalid_backup_file">Skedar rezervë i pavlefshëm</string>
|
||||
<string name="invalid_backup_file_missing_manga">Rezervimi nuk përmban asnjë hyrje në bibliotekë.</string>
|
||||
<string name="invalid_backup_file_missing_manga">Rezervimi nuk përmban asnjë element në bibliotekë.</string>
|
||||
<string name="pref_user_agent_string">Varg i parazgjedhur i agjentit të përdoruesit</string>
|
||||
<string name="pref_clear_chapter_cache">Pastro memorien e kapitullit</string>
|
||||
<string name="pref_clear_database_summary">Fshi historikun për shënimet që nuk janë ruajtur në bibliotekën tënde</string>
|
||||
<string name="pref_clear_database_summary">Fshi historikun për elementet që nuk janë ruajtur në bibliotekën tënde</string>
|
||||
<string name="webview_data_deleted">Të dhënat e WebView u pastruan</string>
|
||||
<string name="pref_reset_viewer_flags_success">Të gjitha cilësimet e lexuesit rivendosen</string>
|
||||
<string name="last_used_source">Përdorur për herë të fundit</string>
|
||||
@ -679,19 +679,19 @@
|
||||
<string name="label_read_chapters">Lexo</string>
|
||||
<string name="not_applicable">N/A</string>
|
||||
<string name="day_short">%dd</string>
|
||||
<string name="label_tracker_section">Ndjekësit</string>
|
||||
<string name="label_tracked_titles">Hyrjet e ndjekura</string>
|
||||
<string name="label_tracker_section">Gjurmuesit</string>
|
||||
<string name="label_tracked_titles">Elementet e gjurmuara</string>
|
||||
<string name="action_not_now">Jo tani</string>
|
||||
<string name="label_completed_titles">Hyrjet e përfunduara</string>
|
||||
<string name="label_completed_titles">Elementet të përfunduara</string>
|
||||
<string name="label_read_duration">Koha e të lezuarit</string>
|
||||
<string name="label_titles_section">Hyrjet</string>
|
||||
<string name="label_titles_section">Elementet</string>
|
||||
<string name="label_titles_in_global_update">Ne përditësimin global</string>
|
||||
<string name="hour_short">%do</string>
|
||||
<string name="minute_short">%dm</string>
|
||||
<string name="information_no_manga_category">Kategorija është bosh</string>
|
||||
<string name="enhanced_services_not_installed">Në dispozicion, por burimi nuk është i instaluar: %s</string>
|
||||
<string name="pref_skip_dupe_chapters">Kapërceni kapitujt e kopjuar</string>
|
||||
<string name="pref_hide_in_library_items">Fshih hyrjet tashmë në bibliotekë</string>
|
||||
<string name="pref_hide_in_library_items">Fshih elementet tashmë në bibliotekë</string>
|
||||
<string name="action_copy_to_clipboard">Kopjo në kujtesën e fragmenteve</string>
|
||||
<string name="confirm_add_duplicate_manga">Ju keni një hyrje në librarni me të njëjtin emër.
|
||||
\n
|
||||
@ -700,4 +700,13 @@
|
||||
<string name="track_error">%1$s gabim: %2$s</string>
|
||||
<string name="information_required_plain">*kërkohet</string>
|
||||
<string name="copied_to_clipboard_plain">U kopjua në clipboard</string>
|
||||
<string name="pref_storage_location">Pika e magazinimit</string>
|
||||
<string name="delete_downloaded">Fshi shkarkimet</string>
|
||||
<string name="action_sort_tracker_score">Piket e gjurmimit</string>
|
||||
<string name="track_delete_title">Hiq %s gjurmimin?</string>
|
||||
<string name="track_delete_remote_text">Hiqe gjithashtu nga %s</string>
|
||||
<string name="track_delete_text">Kjo do te heq gjurmimn lokal.</string>
|
||||
<string name="track_activity_name">Gjurmuesi i identifikimit</string>
|
||||
<string name="pref_relative_format">Koha relative</string>
|
||||
<string name="pref_relative_format_summary">\"%1$s\" në vend të \"%2$s\"</string>
|
||||
</resources>
|
@ -301,9 +301,9 @@
|
||||
<string name="action_sort_latest_chapter">Senaste kapitel</string>
|
||||
<string name="action_view_chapters">Visa kapitel</string>
|
||||
<string name="action_cancel_all">Avbryt alla</string>
|
||||
<string name="theme_light">Av</string>
|
||||
<string name="theme_dark">På</string>
|
||||
<string name="theme_system">Följ systemet</string>
|
||||
<string name="theme_light">Ljus</string>
|
||||
<string name="theme_dark">Mörk</string>
|
||||
<string name="theme_system">System</string>
|
||||
<string name="pref_manage_notifications">Hantera aviseringar</string>
|
||||
<string name="pref_category_security">Säkerhet och integritet</string>
|
||||
<string name="lock_with_biometrics">Kräver upplåsning</string>
|
||||
@ -768,4 +768,29 @@
|
||||
<string name="selected">Vald</string>
|
||||
<string name="not_selected">Inte vald</string>
|
||||
<string name="action_bar_up_description">Navigera upp</string>
|
||||
<string name="onboarding_storage_action_select">Välj en mapp</string>
|
||||
<string name="pref_onboarding_guide">Introduktionsguide</string>
|
||||
<string name="onboarding_guides_new_user">Ny till %s? Vi rekommenderar att du tar en titt på komma igång guiden.</string>
|
||||
<string name="onboarding_action_finish">Kom igång</string>
|
||||
<string name="onboarding_storage_selection_required">En mapp måste väljas</string>
|
||||
<string name="onboarding_heading">Välkommen!</string>
|
||||
<string name="onboarding_guides_returning_user">Redan använt %s förut?</string>
|
||||
<string name="onboarding_action_skip">Hoppa över</string>
|
||||
<string name="onboarding_action_next">Nästa</string>
|
||||
<string name="onboarding_description">Låt oss ställa in några saker först. Du kan alltid ändra dessa i inställningarna senare.</string>
|
||||
<string name="no_location_set">Ingen lagringsplats inställd</string>
|
||||
<string name="onboarding_storage_info">Välj en mapp där %1$s lagrar kapitelnedladdningar, säkerhetskopior och mer.
|
||||
\n
|
||||
\nEn dedikerad mapp rekommenderas.
|
||||
\n
|
||||
\nVald mapp: %2$s</string>
|
||||
<string name="onboarding_permission_notifications">Aviseringsbehörigheter</string>
|
||||
<string name="onboarding_permission_install_apps">Installera app behörigheter</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Undvik avbrott i långvariga biblioteksuppdateringar, nedladdningar och säkerhetskopieringsåterställningar.</string>
|
||||
<string name="onboarding_permission_type_optional">Valfritt</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Bakgrundsbatterianvändning</string>
|
||||
<string name="onboarding_permission_install_apps_description">För att installera källtillägg.</string>
|
||||
<string name="onboarding_permission_notifications_description">Få aviseringar om biblioteksuppdateringar och mer.</string>
|
||||
<string name="onboarding_permission_type_required">Krävs</string>
|
||||
<string name="onboarding_permission_action_grant">Bevilja</string>
|
||||
</resources>
|
@ -301,9 +301,9 @@
|
||||
<string name="action_sort_latest_chapter">Son bölüm</string>
|
||||
<string name="action_view_chapters">Bölümleri görüntüle</string>
|
||||
<string name="action_cancel_all">Hepsini iptal et</string>
|
||||
<string name="theme_light">Kapalı</string>
|
||||
<string name="theme_dark">Açık</string>
|
||||
<string name="theme_system">Sisteme uy</string>
|
||||
<string name="theme_light">Açık</string>
|
||||
<string name="theme_dark">Koyu</string>
|
||||
<string name="theme_system">Sistem</string>
|
||||
<string name="pref_manage_notifications">Bildirimleri yönet</string>
|
||||
<string name="pref_category_security">Güvenlik ve gizlilik</string>
|
||||
<string name="lock_with_biometrics">Kilit açma gerektirir</string>
|
||||
@ -768,4 +768,29 @@
|
||||
<string name="pref_storage_location">Depolama yeri</string>
|
||||
<string name="pref_storage_location_info">Kendiliğinden yedeklemeler, bölüm indirmeleri ve yerel kaynak için kullanılır.</string>
|
||||
<string name="action_bar_up_description">Yukarı git</string>
|
||||
<string name="onboarding_storage_action_select">Klasör seç</string>
|
||||
<string name="pref_onboarding_guide">Başlangıç rehberi</string>
|
||||
<string name="onboarding_guides_new_user">%s\'de yeni misiniz? Başlangıç rehberine göz atmanızı tavsiye ederiz.</string>
|
||||
<string name="onboarding_action_finish">Başlayın</string>
|
||||
<string name="onboarding_storage_selection_required">Bir klasör seçilmelidir</string>
|
||||
<string name="onboarding_heading">Hoş geldiniz!</string>
|
||||
<string name="onboarding_guides_returning_user">Daha önce %s kullandınız mı?</string>
|
||||
<string name="onboarding_action_skip">Atla</string>
|
||||
<string name="onboarding_action_next">Sonraki</string>
|
||||
<string name="onboarding_description">Önce bazı şeyleri ayarlayalım. Bunları daha sonra ayarlardan da değiştirebilirsiniz.</string>
|
||||
<string name="no_location_set">Kaydetme konumu ayarlanmadı</string>
|
||||
<string name="onboarding_storage_info">%1$s bölüm indirmelerini, yedeklemeleri ve başka şeyleri kaydedeceği bir klasör seçin.
|
||||
\n
|
||||
\nYalnızca buna ait bir klasör tavsiye edilir.
|
||||
\n
|
||||
\nSeçilen klasör: %2$s</string>
|
||||
<string name="onboarding_permission_notifications">Bildirim izni</string>
|
||||
<string name="onboarding_permission_install_apps">Uygulama kurma izni</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Uzun süreli kitaplık güncellemeleri, indirmeler ve yedekleme geri yüklemelerinin kesintiye uğramasını önleyin.</string>
|
||||
<string name="onboarding_permission_type_optional">İsteğe bağlı</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Arka planda pil kullanımı</string>
|
||||
<string name="onboarding_permission_install_apps_description">Kaynak uzantılarını kurmak için.</string>
|
||||
<string name="onboarding_permission_notifications_description">Kitaplık güncellemeleri ve daha fazlası için bildirim alın.</string>
|
||||
<string name="onboarding_permission_type_required">Gerekli</string>
|
||||
<string name="onboarding_permission_action_grant">Ver</string>
|
||||
</resources>
|
@ -45,4 +45,7 @@
|
||||
<plurals name="missing_chapters">
|
||||
<item quantity="other">Đang thiếu %1$s</item>
|
||||
</plurals>
|
||||
<plurals name="day">
|
||||
<item quantity="other">%d ngày</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -315,14 +315,14 @@
|
||||
<string name="pref_category_security">Bảo mật và quyền riêng tư</string>
|
||||
<string name="pref_manage_notifications">Quản lý thông báo</string>
|
||||
<string name="pref_date_format">Định dạng ngày</string>
|
||||
<string name="theme_system">Theo hệ thống</string>
|
||||
<string name="theme_dark">Bật</string>
|
||||
<string name="theme_light">Tắt</string>
|
||||
<string name="theme_system">Hệ thống</string>
|
||||
<string name="theme_dark">Tối</string>
|
||||
<string name="theme_light">Sáng</string>
|
||||
<string name="pref_category_library">Thư viện</string>
|
||||
<string name="action_webview_refresh">Làm mới</string>
|
||||
<string name="action_webview_forward">Chuyển tới trước</string>
|
||||
<string name="action_webview_back">Trở lại</string>
|
||||
<string name="action_move_to_bottom">Di chuyển xuống dưới</string>
|
||||
<string name="action_move_to_bottom">Di chuyển xuống cuối</string>
|
||||
<string name="action_move_to_top">Di chuyển lên đầu</string>
|
||||
<string name="action_oldest">Cũ nhất</string>
|
||||
<string name="action_newest">Mới nhất</string>
|
||||
@ -362,7 +362,7 @@
|
||||
<string name="label_downloaded_only">Chỉ hiện truyện đã tải</string>
|
||||
<string name="migration_help_guide">Hướng dẫn di chuyển nguồn</string>
|
||||
<string name="action_search_settings">Cài đặt tìm kiếm</string>
|
||||
<string name="backup_in_progress">Sao lưu đã đang trong quá trình thực hiện</string>
|
||||
<string name="backup_in_progress">Sao lưu đang trong quá trình thực hiện</string>
|
||||
<string name="clear_history_confirmation">Bạn có chắc không\? Tất cả lịch sử sẽ bị xoá.</string>
|
||||
<string name="pref_library_update_categories_details">Truyện trong danh mục bị loại trừ sẽ không được cập nhật.</string>
|
||||
<string name="track_finished_reading_date">Ngày kết thúc</string>
|
||||
@ -528,7 +528,7 @@
|
||||
<string name="pref_hide_threshold">Độ nhạy cho phần tự ẩn mục chính khi kéo cuộn</string>
|
||||
<string name="getting_started_guide">Hướng dẫn sử dụng khởi đầu</string>
|
||||
<string name="pref_tablet_ui_mode">Giao diện máy tính bảng</string>
|
||||
<string name="label_background_activity">Hoạt động ngầm</string>
|
||||
<string name="label_background_activity">Hoạt động nền</string>
|
||||
<string name="pref_lowest">Thấp nhất</string>
|
||||
<string name="pref_low">Thấp</string>
|
||||
<string name="pref_high">Cao</string>
|
||||
@ -635,7 +635,7 @@
|
||||
<string name="remove_manga">Bạn sẽ xóa bỏ \"%s\" này ra khỏi thư viện của bạn</string>
|
||||
<string name="updates_last_update_info">Thư viện lần cuối được cập nhật:%s</string>
|
||||
<string name="download_ahead">Tải Trước</string>
|
||||
<string name="download_ahead_info">Tải trước chỉ áp dụng cho các mục ở trong thư viện và nếu chương cuối cùng đã được tải rồi</string>
|
||||
<string name="download_ahead_info">Tải trước chỉ áp dụng cho các mục ở trong thư viện và nếu chương cuối cùng đã được tải rồi.</string>
|
||||
<string name="multi_lang">Đa Ngôn Ngữ</string>
|
||||
<string name="skipped_reason_not_always_update">Bỏ qua vì loạt truyện không cần cập nhật</string>
|
||||
<string name="action_search_hint">Tìm kiếm…</string>
|
||||
@ -709,7 +709,7 @@
|
||||
<string name="pref_chapter_swipe">Vuốt chương</string>
|
||||
<string name="pref_chapter_swipe_end">Thao tác vuốt sang phải</string>
|
||||
<string name="pref_chapter_swipe_start">Thao tác vuốt sang trái</string>
|
||||
<string name="pref_debug_info">Thông tin Debug</string>
|
||||
<string name="pref_debug_info">Thông tin gỡ lỗi</string>
|
||||
<string name="create_backup_file_error">Không thể tạo tệp sao lưu</string>
|
||||
<string name="action_set_interval">Đặt khoảng thời gian</string>
|
||||
<string name="action_ok">OK</string>
|
||||
@ -727,4 +727,70 @@
|
||||
<string name="unlock_app_title">Mở khoá %s</string>
|
||||
<string name="source_settings">Cài đặt nguồn</string>
|
||||
<string name="app_settings">Cài đặt ứng dụng</string>
|
||||
<string name="pref_storage_location">Vị trí kho chứa</string>
|
||||
<string name="action_create">Tạo</string>
|
||||
<string name="relative_time_span_never">Không bao giờ</string>
|
||||
<string name="onboarding_storage_action_select">Chọn một thư mục</string>
|
||||
<string name="pref_flash_page_summ">Giảm tình trạng bóng ma trên màn giấy điện tử</string>
|
||||
<string name="pref_onboarding_guide">Hướng dẫn làm quen</string>
|
||||
<string name="pref_storage_location_info">Được sử dụng để sao lưu tự động, tải chương và nguồn cục bộ.</string>
|
||||
<string name="onboarding_guides_new_user">Mới với %s sao? Chúng tôi khuyên bạn nên xem hướng dẫn bắt đầu.</string>
|
||||
<string name="onboarding_action_finish">Bắt đầu</string>
|
||||
<string name="action_apply">Áp dụng</string>
|
||||
<string name="manga_display_modified_interval_title">Đặt để cập nhật mỗi</string>
|
||||
<string name="onboarding_storage_selection_required">Phải chọn một thư mục</string>
|
||||
<string name="onboarding_permission_notifications">Quyền thông báo</string>
|
||||
<string name="intervals_header">Đoạn thời gian</string>
|
||||
<string name="action_revert_to_default">Chỉnh về mặc định</string>
|
||||
<string name="onboarding_permission_install_apps">Cài đặt quyền ứng dụng</string>
|
||||
<string name="action_sort_category">Lọc danh mục</string>
|
||||
<string name="onboarding_heading">Xin chào!</string>
|
||||
<string name="action_menu_overflow_description">Thêm tùy chọn</string>
|
||||
<string name="last_auto_backup_info">Lần cuối cùng tự động lưu: %s</string>
|
||||
<string name="onboarding_guides_returning_user">Đã sử dụng %s từ trước rồi sao?</string>
|
||||
<string name="manga_modify_calculated_interval_title">Tùy chỉnh đoạn thời gian</string>
|
||||
<string name="selected">Được chọn</string>
|
||||
<string name="no_scanlators_found">Không tìm thấy máy quét nào</string>
|
||||
<string name="not_selected">Chưa chọn</string>
|
||||
<string name="action_move_to_bottom_all_for_series">Di chuyển bộ truyện xuống cuối</string>
|
||||
<string name="scanlator">Máy quét</string>
|
||||
<string name="onboarding_action_skip">Bỏ qua</string>
|
||||
<string name="pref_flash_page">Hiện trắng khi đổi trang</string>
|
||||
<string name="licensed_manga_chapters_error">Được cấp phép - Không có chương nào để hiển thị</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Tránh gián đoạn quá trình cập nhật thư viện, tải xuống và khôi phục bản sao lưu trong thời gian dài.</string>
|
||||
<string name="exception_offline">Mất kết nối mạng</string>
|
||||
<string name="pref_storage_usage">Kho chứa chiếm dụng</string>
|
||||
<string name="onboarding_permission_type_optional">Tùy ý</string>
|
||||
<string name="onboarding_action_next">Tiếp</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Sử dụng pin nền</string>
|
||||
<string name="onboarding_permission_install_apps_description">Để cài đặt nguồn mở rộng.</string>
|
||||
<string name="notification_updating_progress">Đang cập nhật thư viện… (%s)</string>
|
||||
<string name="download_cache_invalidated">Chỉ mục tải xuống bị vô hiệu</string>
|
||||
<string name="action_bar_up_description">Điều hướng trên</string>
|
||||
<string name="onboarding_description">Trước tiên hãy thiết lập một số thứ nhé. Bạn có thể tùy ý chỉnh lại những cài đặt này lại sau.</string>
|
||||
<string name="action_sort_tracker_score">Điểm bộ theo dõi</string>
|
||||
<string name="no_location_set">Chưa đặt vị trí kho chứa</string>
|
||||
<string name="label_data_storage">Dữ liệu và kho chứa</string>
|
||||
<string name="sort_category_confirmation">Bạn có muốn lọc danh mục theo thứ tự bảng chữ cái?</string>
|
||||
<string name="skipped_reason_not_in_release_period">Bỏ qua vì dự kiến hôm nay không có bản phát hành nào</string>
|
||||
<string name="onboarding_permission_notifications_description">Gửi thông báo khi thư viện cấp nhật và nhiều hơn thế.</string>
|
||||
<string name="file_null_uri_error">Không có tập tin được chọn</string>
|
||||
<string name="track_delete_title">Xóa bộ theo dõi %s?</string>
|
||||
<string name="track_delete_remote_text">Đồng thời xóa khỏi %s</string>
|
||||
<string name="onboarding_permission_type_required">Bắt buộc</string>
|
||||
<string name="has_results">Có kết quả</string>
|
||||
<string name="manga_display_interval_title">Ước tính mỗi</string>
|
||||
<string name="track_delete_text">Điều này sẽ loại bỏ bộ theo dõi cục bộ.</string>
|
||||
<string name="track_activity_name">Đăng nhập bộ theo dõi</string>
|
||||
<string name="onboarding_permission_action_grant">Cho phép</string>
|
||||
<string name="onboarding_storage_info">Chọn thư mục nơi mà %1$s sẽ chứa chương truyện tải xuống, sao lưu, và những thứ khác.
|
||||
\n
|
||||
\nKhuyến khích sử dụng một thư mục chuyên dụng.
|
||||
\n
|
||||
\nThư mục được chọn: %2$s</string>
|
||||
<string name="pref_relative_format">Mốc thời gian liên quan</string>
|
||||
<string name="exception_http">HTTP %d, kiểm tra trang web trong WebView</string>
|
||||
<string name="pref_relative_format_summary">\"%1$s\" thay vì là \"%2$s\"</string>
|
||||
<string name="exception_unknown_host">Không thể truy cập %s</string>
|
||||
<string name="exclude_scanlators">Loại trừ máy quét</string>
|
||||
</resources>
|
@ -762,4 +762,7 @@
|
||||
<string name="scanlator">扫译者</string>
|
||||
<string name="action_sort_tracker_score">记录平台评分</string>
|
||||
<string name="exclude_scanlators">排除的扫译者</string>
|
||||
<string name="action_menu_overflow_description">更多选项</string>
|
||||
<string name="selected">已选择</string>
|
||||
<string name="not_selected">未选择</string>
|
||||
</resources>
|
@ -291,9 +291,9 @@
|
||||
<string name="pref_category_library">書櫃</string>
|
||||
<string name="ext_obsolete">過舊</string>
|
||||
<string name="obsolete_extension_message">這個擴充套件已無法使用,其可能無法正確運作或導致本程式發生問題。建議解除安裝。</string>
|
||||
<string name="theme_light">關閉</string>
|
||||
<string name="theme_system">遵循系統</string>
|
||||
<string name="theme_dark">開啟</string>
|
||||
<string name="theme_light">淺色</string>
|
||||
<string name="theme_system">系統</string>
|
||||
<string name="theme_dark">深色</string>
|
||||
<string name="pref_date_format">日期格式</string>
|
||||
<string name="downloaded_only_summary">將套用至你書櫃中的作品</string>
|
||||
<string name="label_downloaded_only">僅限下載內容</string>
|
||||
@ -643,7 +643,7 @@
|
||||
<string name="pref_appearance_summary">主題、日期格式</string>
|
||||
<string name="pref_downloads_summary">自動下載、預先下載</string>
|
||||
<string name="pref_tracking_summary">單向進度同步、增強式同步</string>
|
||||
<string name="pref_backup_summary">手動與自動備份,儲存空間</string>
|
||||
<string name="pref_backup_summary">手動與自動備份、儲存空間</string>
|
||||
<string name="pref_security_summary">上鎖應用程式、防窺畫面</string>
|
||||
<string name="pref_advanced_summary">傾印當機記錄、電池效能最佳化</string>
|
||||
<string name="crash_screen_restart_application">重新啟動應用程式</string>
|
||||
@ -750,9 +750,9 @@
|
||||
<string name="source_settings">來源設定</string>
|
||||
<string name="file_null_uri_error">未選擇檔案</string>
|
||||
<string name="relative_time_span_never">永不</string>
|
||||
<string name="pref_flash_page_summ">減少電子墨水螢幕上的殘影</string>
|
||||
<string name="pref_flash_page_summ">減少電子紙顯示器上的殘影</string>
|
||||
<string name="last_auto_backup_info">最後一次自動備份:%s</string>
|
||||
<string name="pref_flash_page">頁面轉換時閃白</string>
|
||||
<string name="pref_flash_page">翻頁時閃爍白畫面</string>
|
||||
<string name="label_data_storage">資料與儲存空間</string>
|
||||
<string name="pref_storage_usage">儲存空間使用情形</string>
|
||||
<string name="action_sort_tracker_score">歷程平台評分</string>
|
||||
@ -768,4 +768,29 @@
|
||||
<string name="action_bar_up_description">向上瀏覽</string>
|
||||
<string name="pref_storage_location">儲存位置</string>
|
||||
<string name="pref_storage_location_info">供自動備份、章節下載和本機來源使用。</string>
|
||||
<string name="onboarding_storage_action_select">選擇資料夾</string>
|
||||
<string name="pref_onboarding_guide">新手上路精靈</string>
|
||||
<string name="onboarding_guides_new_user">初探 %s?我們建議你查看入門指南。</string>
|
||||
<string name="onboarding_action_finish">開始使用</string>
|
||||
<string name="onboarding_storage_selection_required">必須選擇一個資料夾</string>
|
||||
<string name="onboarding_heading">歡迎!</string>
|
||||
<string name="onboarding_guides_returning_user">已是 %s 的既有使用者?</string>
|
||||
<string name="onboarding_action_skip">略過</string>
|
||||
<string name="onboarding_action_next">下一步</string>
|
||||
<string name="onboarding_description">讓我們先設定一些東西。稍後你隨時可至設定中變更這些選項。</string>
|
||||
<string name="no_location_set">未設定儲存位置</string>
|
||||
<string name="onboarding_storage_info">選擇供 %1$s 存放下載的章節、備份檔等內容的資料夾。
|
||||
\n
|
||||
\n建議使用專屬的資料夾。
|
||||
\n
|
||||
\n選擇的資料夾:%2$s</string>
|
||||
<string name="onboarding_permission_notifications">通知權限</string>
|
||||
<string name="onboarding_permission_install_apps">安裝應用程式權限</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">避免中斷書櫃更新、章節下載和還原備份等較為費時的作業。</string>
|
||||
<string name="onboarding_permission_type_optional">選用</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">背景耗電量</string>
|
||||
<string name="onboarding_permission_install_apps_description">用以安裝來源擴充套件。</string>
|
||||
<string name="onboarding_permission_notifications_description">用以傳送書櫃更新等通知。</string>
|
||||
<string name="onboarding_permission_type_required">必要</string>
|
||||
<string name="onboarding_permission_action_grant">授予</string>
|
||||
</resources>
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Newspaper
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBarDefaults
|
||||
@ -38,6 +39,7 @@ fun InfoScreen(
|
||||
subtitleText: String,
|
||||
acceptText: String,
|
||||
onAcceptClick: () -> Unit,
|
||||
canAccept: Boolean = true,
|
||||
rejectText: String? = null,
|
||||
onRejectClick: (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
@ -63,8 +65,9 @@ fun InfoScreen(
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
) {
|
||||
androidx.compose.material3.Button(
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = canAccept,
|
||||
onClick = onAcceptClick,
|
||||
) {
|
||||
Text(text = acceptText)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package tachiyomi.presentation.core.util
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@ -16,6 +15,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
@ -28,7 +28,10 @@ import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = if (isSelected) {
|
||||
composed {
|
||||
val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f
|
||||
Modifier.background(MaterialTheme.colorScheme.secondary.copy(alpha = alpha))
|
||||
val color = MaterialTheme.colorScheme.secondary.copy(alpha = alpha)
|
||||
Modifier.drawBehind {
|
||||
drawRect(color)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this
|
||||
|
Loading…
Reference in New Issue
Block a user