chore: merge upstream.

This commit is contained in:
KaiserBh 2023-12-20 02:46:21 +11:00
commit 18fb63cd01
81 changed files with 1871 additions and 1160 deletions

View File

@ -3,7 +3,8 @@ on:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.md' - '**.md'
- 'i18n/src/main/res/**/strings.xml' - 'i18n/src/commonMain/resources/**/strings.xml'
- 'i18n/src/commonMain/resources/**/plurals.xml'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}

View File

@ -22,7 +22,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 112 versionCode = 113
versionName = "0.14.7" versionName = "0.14.7"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
@ -123,6 +123,7 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
compose = true compose = true
buildConfig = true
// Disable some unused things // Disable some unused things
aidl = false aidl = false

View File

@ -160,7 +160,8 @@
<service <service
android:name=".extension.util.ExtensionInstallService" android:name=".extension.util.ExtensionInstallService"
android:exported="false" /> android:exported="false"
android:foregroundServiceType="shortService" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"

View File

@ -62,15 +62,15 @@ private const val GridSelectedCoverAlpha = 0.76f
*/ */
@Composable @Composable
fun MangaCompactGridItem( fun MangaCompactGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover,
onClick: () -> Unit,
onLongClick: () -> Unit,
isSelected: Boolean = false, isSelected: Boolean = false,
title: String? = null, title: String? = null,
coverData: tachiyomi.domain.manga.model.MangaCover, onClickContinueReading: (() -> Unit)? = null,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
coverBadgeStart: @Composable (RowScope.() -> Unit)? = null, coverBadgeStart: @Composable (RowScope.() -> Unit)? = null,
coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null, coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null,
) { ) {
GridItemSelectable( GridItemSelectable(
isSelected = isSelected, isSelected = isSelected,
@ -163,15 +163,15 @@ private fun BoxScope.CoverTextOverlay(
*/ */
@Composable @Composable
fun MangaComfortableGridItem( fun MangaComfortableGridItem(
isSelected: Boolean = false,
title: String,
titleMaxLines: Int = 2,
coverData: tachiyomi.domain.manga.model.MangaCover, coverData: tachiyomi.domain.manga.model.MangaCover,
title: String,
onClick: () -> Unit,
onLongClick: () -> Unit,
isSelected: Boolean = false,
titleMaxLines: Int = 2,
coverAlpha: Float = 1f, coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null, coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
onClickContinueReading: (() -> Unit)? = null, onClickContinueReading: (() -> Unit)? = null,
) { ) {
GridItemSelectable( GridItemSelectable(
@ -253,10 +253,10 @@ private fun MangaGridCover(
@Composable @Composable
private fun GridItemTitle( private fun GridItemTitle(
modifier: Modifier,
title: String, title: String,
style: TextStyle, style: TextStyle,
minLines: Int, minLines: Int,
modifier: Modifier = Modifier,
maxLines: Int = 2, maxLines: Int = 2,
) { ) {
Text( Text(
@ -276,10 +276,10 @@ private fun GridItemTitle(
*/ */
@Composable @Composable
private fun GridItemSelectable( private fun GridItemSelectable(
modifier: Modifier = Modifier,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
Box( Box(
@ -316,13 +316,13 @@ private fun Modifier.selectedOutline(
*/ */
@Composable @Composable
fun MangaListItem( fun MangaListItem(
isSelected: Boolean = false,
title: String,
coverData: tachiyomi.domain.manga.model.MangaCover, coverData: tachiyomi.domain.manga.model.MangaCover,
coverAlpha: Float = 1f, title: String,
badge: @Composable (RowScope.() -> Unit),
onLongClick: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit,
badge: @Composable (RowScope.() -> Unit),
isSelected: Boolean = false,
coverAlpha: Float = 1f,
onClickContinueReading: (() -> Unit)? = null, onClickContinueReading: (() -> Unit)? = null,
) { ) {
Row( Row(

View File

@ -33,7 +33,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@ -189,7 +188,7 @@ fun MangaChapterListItem(
text = readProgress, text = readProgress,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(ReadItemAlpha), color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
) )
if (scanlator != null) DotSeparatorText() if (scanlator != null) DotSeparatorText()
} }

View File

@ -124,7 +124,7 @@ fun MangaInfoBox(
) )
} }
.blur(4.dp) .blur(4.dp)
.alpha(.2f), .alpha(0.2f),
) )
// Manga & source info // Manga & source info

View File

@ -35,10 +35,8 @@ import tachiyomi.presentation.core.theme.active
@Composable @Composable
fun MangaToolbar( fun MangaToolbar(
modifier: Modifier = Modifier,
title: String, title: String,
titleAlphaProvider: () -> Float, titleAlphaProvider: () -> Float,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
hasFilters: Boolean, hasFilters: Boolean,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
onClickFilter: () -> Unit, onClickFilter: () -> Unit,
@ -47,10 +45,14 @@ fun MangaToolbar(
onClickEditCategory: (() -> Unit)?, onClickEditCategory: (() -> Unit)?,
onClickRefresh: () -> Unit, onClickRefresh: () -> Unit,
onClickMigrate: (() -> Unit)?, onClickMigrate: (() -> Unit)?,
// For action mode // For action mode
actionModeCounter: Int, actionModeCounter: Int,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
modifier: Modifier = Modifier,
backgroundAlphaProvider: () -> Float = titleAlphaProvider,
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
@ -62,7 +64,7 @@ fun MangaToolbar(
text = if (isActionMode) actionModeCounter.toString() else title, text = if (isActionMode) actionModeCounter.toString() else title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(if (isActionMode) 1f else titleAlphaProvider()), color = LocalContentColor.current.copy(alpha = if (isActionMode) 1f else titleAlphaProvider()),
) )
}, },
navigationIcon = { navigationIcon = {

View File

@ -17,10 +17,13 @@ import eu.kanade.presentation.theme.TachiyomiTheme
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable internal class GuidesStep(
internal fun GuidesStep( private val onRestoreBackup: () -> Unit,
onRestoreBackup: () -> Unit, ) : OnboardingStep {
) { override val isComplete: Boolean = true
@Composable
override fun Content() {
val handler = LocalUriHandler.current val handler = LocalUriHandler.current
Column( Column(
@ -47,6 +50,7 @@ internal fun GuidesStep(
Text(stringResource(MR.strings.pref_restore_backup)) Text(stringResource(MR.strings.pref_restore_backup))
} }
} }
}
} }
const val GETTING_STARTED_URL = "https://tachiyomi.org/docs/guides/getting-started" const val GETTING_STARTED_URL = "https://tachiyomi.org/docs/guides/getting-started"
@ -57,6 +61,6 @@ private fun GuidesStepPreview() {
TachiyomiTheme { TachiyomiTheme {
GuidesStep( GuidesStep(
onRestoreBackup = {}, onRestoreBackup = {},
) ).Content()
} }
} }

View File

@ -13,15 +13,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.materialSharedAxisX
import soup.compose.material.motion.animation.rememberSlideDistance import soup.compose.material.motion.animation.rememberSlideDistance
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@ -29,24 +26,21 @@ import tachiyomi.presentation.core.screens.InfoScreen
@Composable @Composable
fun OnboardingScreen( fun OnboardingScreen(
storagePreferences: StoragePreferences,
uiPreferences: UiPreferences,
onComplete: () -> Unit, onComplete: () -> Unit,
onRestoreBackup: () -> Unit, onRestoreBackup: () -> Unit,
) { ) {
val context = LocalContext.current
val slideDistance = rememberSlideDistance() val slideDistance = rememberSlideDistance()
var currentStep by remember { mutableIntStateOf(0) } var currentStep by rememberSaveable { mutableIntStateOf(0) }
val steps: List<@Composable () -> Unit> = remember { val steps = remember {
listOf( listOf(
{ ThemeStep(uiPreferences = uiPreferences) }, ThemeStep(),
{ StorageStep(storagePref = storagePreferences.baseStorageDirectory()) }, StorageStep(),
// TODO: prompt for notification permissions when bumping target to Android 13 PermissionStep(),
{ GuidesStep(onRestoreBackup = onRestoreBackup) }, GuidesStep(onRestoreBackup = onRestoreBackup),
) )
} }
val isLastStep = currentStep == steps.size - 1 val isLastStep = currentStep == steps.lastIndex
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- }) BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
@ -61,17 +55,13 @@ fun OnboardingScreen(
MR.strings.onboarding_action_next MR.strings.onboarding_action_next
}, },
), ),
canAccept = steps[currentStep].isComplete,
onAcceptClick = { onAcceptClick = {
if (isLastStep) { if (isLastStep) {
onComplete() onComplete()
} else {
// TODO: this is kind of janky
if (currentStep == 1 && !storagePreferences.baseStorageDirectory().isSet()) {
context.toast(MR.strings.onboarding_storage_selection_required)
} else { } else {
currentStep++ currentStep++
} }
}
}, },
) { ) {
Box( Box(
@ -91,7 +81,7 @@ fun OnboardingScreen(
}, },
label = "stepContent", label = "stepContent",
) { ) {
steps[it]() steps[it].Content()
} }
} }
} }

View File

@ -0,0 +1,11 @@
package eu.kanade.presentation.more.onboarding
import androidx.compose.runtime.Composable
internal interface OnboardingStep {
val isComplete: Boolean
@Composable
fun Content()
}

View File

@ -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),
)
}
}

View File

@ -7,20 +7,34 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
import eu.kanade.tachiyomi.util.system.toast 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.i18n.MR
import tachiyomi.presentation.core.components.material.Button import tachiyomi.presentation.core.components.material.Button
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable internal class StorageStep : OnboardingStep {
internal fun StorageStep(
storagePref: Preference<String>, private val storagePref = Injekt.get<StoragePreferences>().baseStorageDirectory()
) {
private var _isComplete by mutableStateOf(false)
override val isComplete: Boolean
get() = _isComplete
@Composable
override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref) val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
@ -49,4 +63,10 @@ internal fun StorageStep(
Text(stringResource(MR.strings.onboarding_storage_action_select)) Text(stringResource(MR.strings.onboarding_storage_action_select))
} }
} }
LaunchedEffect(Unit) {
storagePref.changes()
.collectLatest { _isComplete = storagePref.isSet() }
}
}
} }

View File

@ -8,11 +8,17 @@ import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@Composable internal class ThemeStep : OnboardingStep {
internal fun ThemeStep(
uiPreferences: UiPreferences, override val isComplete: Boolean = true
) {
private val uiPreferences: UiPreferences = Injekt.get()
@Composable
override fun Content() {
val themeModePref = uiPreferences.themeMode() val themeModePref = uiPreferences.themeMode()
val themeMode by themeModePref.collectAsState() val themeMode by themeModePref.collectAsState()
@ -37,4 +43,5 @@ internal fun ThemeStep(
onItemClick = { appThemePref.set(it) }, onItemClick = { appThemePref.set(it) },
) )
} }
}
} }

View File

@ -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.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
import eu.kanade.presentation.util.relativeTimeSpanString 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.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.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncDataJob
@ -107,7 +107,6 @@ object SettingsDataScreen : SearchableSettings {
UniFile.fromUri(context, uri)?.let { UniFile.fromUri(context, uri)?.let {
storageDirPref.set(it.uri.toString()) storageDirPref.set(it.uri.toString())
} }
Injekt.get<DownloadCache>().invalidateCache()
} }
} }
} }

View File

@ -29,8 +29,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast

View File

@ -248,7 +248,6 @@ private fun TrackDetailsItem(
Box( Box(
modifier = modifier modifier = modifier
.clickable(onClick = onClick) .clickable(onClick = onClick)
.alpha(if (text == null) UnsetStatusTextAlpha else 1f)
.fillMaxHeight() .fillMaxHeight()
.padding(12.dp), .padding(12.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@ -259,7 +258,7 @@ private fun TrackDetailsItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface.copy(alpha = if (text == null) UnsetStatusTextAlpha else 1f),
) )
} }
} }

View File

@ -7,7 +7,7 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences 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.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
@ -66,10 +66,6 @@ object Migrations {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (oldVersion < 14) {
// Restore jobs after upgrading to Evernote's job scheduler.
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 15) { if (oldVersion < 15) {
// Delete internal chapter cache dir. // Delete internal chapter cache dir.
File(context.cacheDir, "chapter_disk_cache").deleteRecursively() 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) { if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source // Reset sorting preference if using removed sort by source
val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0) val oldSortingMode = prefs.getInt(libraryPreferences.sortingMode().key(), 0)
@ -259,9 +250,6 @@ object Migrations {
basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY) basePreferences.extensionInstaller().set(BasePreferences.ExtensionInstaller.LEGACY)
} }
} }
if (oldVersion < 76) {
BackupCreateJob.setupTask(context)
}
if (oldVersion < 77) { if (oldVersion < 77) {
val oldReaderTap = prefs.getBoolean("reader_tap", false) val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) { if (!oldReaderTap) {
@ -374,9 +362,6 @@ object Migrations {
} }
} }
} }
if (oldVersion < 100) {
BackupCreateJob.setupTask(context)
}
if (oldVersion < 105) { if (oldVersion < 105) {
val pref = libraryPreferences.autoUpdateDeviceRestrictions() val pref = libraryPreferences.autoUpdateDeviceRestrictions()
if (pref.isSet() && "battery_not_low" in pref.get()) { if (pref.isSet() && "battery_not_low" in pref.get()) {
@ -396,12 +381,7 @@ object Migrations {
newKey = { Preference.privateKey(it) }, newKey = { Preference.privateKey(it) },
) )
} }
if (oldVersion < 111) { if (oldVersion < 113) {
File(context.cacheDir, "dl_index_cache")
.takeIf { it.exists() }
?.delete()
}
if (oldVersion < 112) {
val prefsToReplace = listOf( val prefsToReplace = listOf(
"pref_download_only", "pref_download_only",
"incognito_mode", "incognito_mode",
@ -421,6 +401,9 @@ object Migrations {
filterPredicate = { it.key in prefsToReplace }, filterPredicate = { it.key in prefsToReplace },
newKey = { Preference.appStateKey(it) }, newKey = { Preference.appStateKey(it) },
) )
// Deleting old download cache index files, but might as well clear it all out
context.cacheDir.deleteRecursively()
} }
return true return true
} }

View File

@ -68,18 +68,18 @@ class BackupNotifier(private val context: Context) {
} }
} }
fun showBackupComplete(unifile: UniFile) { fun showBackupComplete(file: UniFile) {
context.cancelNotification(Notifications.ID_BACKUP_PROGRESS) context.cancelNotification(Notifications.ID_BACKUP_PROGRESS)
with(completeNotificationBuilder) { with(completeNotificationBuilder) {
setContentTitle(context.stringResource(MR.strings.backup_created)) setContentTitle(context.stringResource(MR.strings.backup_created))
setContentText(unifile.filePath ?: unifile.name) setContentText(file.filePath ?: file.name)
clearActions() clearActions()
addAction( addAction(
R.drawable.ic_share_24dp, R.drawable.ic_share_24dp,
context.stringResource(MR.strings.action_share), context.stringResource(MR.strings.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri), NotificationReceiver.shareBackupPendingBroadcast(context, file.uri),
) )
show(Notifications.ID_BACKUP_COMPLETE) show(Notifications.ID_BACKUP_COMPLETE)
@ -88,13 +88,16 @@ class BackupNotifier(private val context: Context) {
fun showRestoreProgress( fun showRestoreProgress(
content: String = "", content: String = "",
contentTitle: String = context.stringResource(
MR.strings.restoring_backup,
),
progress: Int = 0, progress: Int = 0,
maxAmount: Int = 100, maxAmount: Int = 100,
sync: Boolean = false,
): NotificationCompat.Builder { ): NotificationCompat.Builder {
val builder = with(progressNotificationBuilder) { val builder = with(progressNotificationBuilder) {
val contentTitle = if (sync) {
context.stringResource(MR.strings.syncing_library)
} else {
context.stringResource(MR.strings.restoring_backup)
}
setContentTitle(contentTitle) setContentTitle(contentTitle)
if (!preferences.hideNotificationContent().get()) { if (!preferences.hideNotificationContent().get()) {
@ -133,10 +136,14 @@ class BackupNotifier(private val context: Context) {
errorCount: Int, errorCount: Int,
path: String?, path: String?,
file: String?, file: String?,
contentTitle: String = context.stringResource( sync: Boolean,
MR.strings.restore_completed,
),
) { ) {
val contentTitle = if (sync) {
context.stringResource(MR.strings.library_sync_complete)
} else {
context.stringResource(MR.strings.restore_completed)
}
context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) context.cancelNotification(Notifications.ID_RESTORE_PROGRESS)
val timeString = context.stringResource( val timeString = context.stringResource(

View File

@ -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)
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup.create
internal object BackupCreateFlags { internal object BackupCreateFlags {
const val BACKUP_CATEGORY = 0x1 const val BACKUP_CATEGORY = 0x1

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup.create
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.Constraints import androidx.work.Constraints
@ -14,6 +16,8 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.hippo.unifile.UniFile 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.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
@ -68,6 +72,11 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
return ForegroundInfo( return ForegroundInfo(
Notifications.ID_BACKUP_PROGRESS, Notifications.ID_BACKUP_PROGRESS,
notifier.showBackupProgress().build(), notifier.showBackupProgress().build(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
) )
} }

View File

@ -1,14 +1,15 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup.create
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK 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.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter import eu.kanade.tachiyomi.data.backup.models.BackupChapter

View File

@ -2,13 +2,22 @@ package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.history.model.History
import java.util.Date
@Serializable @Serializable
data class BackupHistory( data class BackupHistory(
@ProtoNumber(1) var url: String, @ProtoNumber(1) var url: String,
@ProtoNumber(2) var lastRead: Long, @ProtoNumber(2) var lastRead: Long,
@ProtoNumber(3) var readDuration: Long = 0, @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.") @Deprecated("Replaced with BackupHistory. This is retained for legacy reasons.")
@Serializable @Serializable
@ -16,4 +25,8 @@ data class BrokenBackupHistory(
@ProtoNumber(0) var url: String, @ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long, @ProtoNumber(1) var lastRead: Long,
@ProtoNumber(2) var readDuration: Long = 0, @ProtoNumber(2) var readDuration: Long = 0,
) ) {
fun toBackupHistory(): BackupHistory {
return BackupHistory(url, lastRead, readDuration)
}
}

View File

@ -4,9 +4,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Serializable @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 { companion object {
fun copyFrom(manga: Manga): BackupManga { fun copyFrom(manga: Manga): BackupManga {
return BackupManga( return BackupManga(

View File

@ -8,7 +8,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class BrokenBackupSource( data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "", @ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long, @ProtoNumber(1) var sourceId: Long,
) ) {
fun toBackupSource() = BackupSource(name, sourceId)
}
@Serializable @Serializable
data class BackupSource( data class BackupSource(

View File

@ -30,7 +30,7 @@ data class BackupTracking(
) { ) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun getTrackingImpl(): Track { fun getTrackImpl(): Track {
return Track( return Track(
id = -1, id = -1,
mangaId = -1, mangaId = -1,

View File

@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup.restore
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
@ -9,6 +11,7 @@ import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
@ -28,13 +31,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: return Result.failure() ?: return Result.failure()
val sync = inputData.getBoolean(SYNC_KEY, false) val isSync = inputData.getBoolean(SYNC_KEY, false)
setForegroundSafely() setForegroundSafely()
return try { return try {
val restorer = BackupRestorer(context, notifier) BackupRestorer(context, notifier, isSync).restore(uri)
restorer.syncFromBackup(uri, sync)
Result.success() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) { if (e is CancellationException) {
@ -54,6 +56,11 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
return ForegroundInfo( return ForegroundInfo(
Notifications.ID_RESTORE_PROGRESS, Notifications.ID_RESTORE_PROGRESS,
notifier.showRestoreProgress().build(), notifier.showRestoreProgress().build(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
) )
} }

View File

@ -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("")
}
}

View 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,
)
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}
}
}
}
}

View File

@ -18,7 +18,6 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
@ -48,7 +47,7 @@ import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -66,7 +65,7 @@ class DownloadCache(
private val provider: DownloadProvider = Injekt.get(), private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
storagePreferences: StoragePreferences = Injekt.get(), private val storageManager: StorageManager = Injekt.get(),
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
@ -74,7 +73,7 @@ class DownloadCache(
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED) private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
val changes = _changes.receiveAsFlow() val changes = _changes.receiveAsFlow()
.onStart { emit(Unit) } .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 * 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) .stateIn(scope, SharingStarted.WhileSubscribed(), false)
private val diskCacheFile: File 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 val rootDownloadsDirLock = Mutex()
private var rootDownloadsDir = RootDirectory(provider.downloadsDir) private var rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
init { init {
// Attempt to read cache file // Attempt to read cache file
@ -115,12 +114,8 @@ class DownloadCache(
} }
} }
storagePreferences.baseStorageDirectory().changes() storageManager.changes
.drop(1) .onEach { invalidateCache() }
.onEach {
rootDownloadsDir = RootDirectory(provider.downloadsDir)
invalidateCache()
}
.launchIn(scope) .launchIn(scope)
} }
@ -294,6 +289,8 @@ class DownloadCache(
fun invalidateCache() { fun invalidateCache() {
lastRenew = 0L lastRenew = 0L
renewalJob?.cancel() renewalJob?.cancel()
diskCacheFile.delete()
renewCache()
} }
/** /**
@ -310,23 +307,26 @@ class DownloadCache(
_isInitializing.emit(true) _isInitializing.emit(true)
} }
var sources = getSources()
// Try to wait until extensions and sources have loaded // Try to wait until extensions and sources have loaded
var sources = getSources()
if (sources.isEmpty()) {
withTimeoutOrNull(30.seconds) { withTimeoutOrNull(30.seconds) {
while (!extensionManager.isInitialized) { while (!extensionManager.isInitialized) {
delay(2.seconds) delay(2.seconds)
} }
while (sources.isEmpty()) { while (extensionManager.availableExtensionsFlow.value.isNotEmpty() && sources.isEmpty()) {
delay(2.seconds) delay(2.seconds)
sources = getSources() sources = getSources()
} }
} }
}
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirLock.withLock { rootDownloadsDirLock.withLock {
rootDownloadsDir = RootDirectory(storageManager.getDownloadsDirectory())
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() } .filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir -> .mapNotNull { dir ->
@ -371,10 +371,9 @@ class DownloadCache(
}.also { }.also {
it.invokeOnCompletion(onCancelling = true) { exception -> it.invokeOnCompletion(onCancelling = true) { exception ->
if (exception != null && exception !is CancellationException) { 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() lastRenew = System.currentTimeMillis()
notifyChanges() notifyChanges()
} }
} }

View File

@ -13,13 +13,17 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.NetworkState
import eu.kanade.tachiyomi.util.system.isOnline 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.notificationBuilder
import eu.kanade.tachiyomi.util.system.setForegroundSafely import eu.kanade.tachiyomi.util.system.setForegroundSafely
import kotlinx.coroutines.delay import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -50,7 +54,11 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
} }
override suspend fun doWork(): Result { 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) { if (!active) {
return Result.failure() return Result.failure()
@ -58,19 +66,27 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
setForegroundSafely() 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 // Keep the worker running when needed
while (active) { while (active) {
delay(100) active = !isStopped && downloadManager.isRunning && networkCheck
active = !isStopped && downloadManager.isRunning && checkConnectivity()
} }
return Result.success() return Result.success()
} }
private fun checkConnectivity(): Boolean { private fun checkNetworkState(state: NetworkState, requireWifi: Boolean): Boolean {
return with(applicationContext) { return if (state.isOnline) {
if (isOnline()) { val noWifi = requireWifi && !state.isWifi
val noWifi = downloadPreferences.downloadOnlyOverWifi().get() && !isConnectedToWifi()
if (noWifi) { if (noWifi) {
downloadManager.downloaderStop( downloadManager.downloaderStop(
applicationContext.getString(R.string.download_notifier_text_only_wifi), applicationContext.getString(R.string.download_notifier_text_only_wifi),
@ -82,7 +98,6 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
false false
} }
} }
}
companion object { companion object {
private const val TAG = "Downloader" private const val TAG = "Downloader"

View File

@ -7,7 +7,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.net.toUri 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.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.sync.SyncDataJob import eu.kanade.tachiyomi.data.sync.SyncDataJob

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
@ -55,6 +57,11 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
return ForegroundInfo( return ForegroundInfo(
Notifications.ID_APP_UPDATER, Notifications.ID_APP_UPDATER,
notifier.onDownloadStarted().build(), notifier.onDownloadStarted().build(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
) )
} }

View File

@ -9,6 +9,7 @@ import android.content.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.IntentSanitizer
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.lang.use import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat 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)) { when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT) 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) { if (userAction == null) {
logcat(LogPriority.ERROR) { "Fatal error for $intent" } logcat(LogPriority.ERROR) { "Fatal error for $intent" }
continueQueue(InstallStep.Error) continueQueue(InstallStep.Error)
@ -71,13 +86,13 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
val intentSender = PendingIntent.getBroadcast( val intentSender = PendingIntent.getBroadcast(
service, service,
activeSession!!.second, 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, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
).intentSender ).intentSender
session.commit(intentSender) session.commit(intentSender)
} }
} catch (e: Exception) { } 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) -> activeSession?.let { (_, sessionId) ->
packageInstaller.abandonSession(sessionId) packageInstaller.abandonSession(sessionId)
} }
@ -105,7 +120,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
service, service,
packageActionReceiver, packageActionReceiver,
IntentFilter(INSTALL_ACTION), IntentFilter(INSTALL_ACTION),
ContextCompat.RECEIVER_EXPORTED, ContextCompat.RECEIVER_NOT_EXPORTED,
) )
} }
} }

View File

@ -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
}

View File

@ -65,8 +65,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
.fillMaxSize() .fillMaxSize()
.padding(contentPadding), .padding(contentPadding),
) { ) {
val fragment = SourcePreferencesFragment.getInstance(sourceId) add(it, SourcePreferencesFragment.getInstance(sourceId), null)
add(it, fragment, null)
} }
} }
} }
@ -127,12 +126,13 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
private fun populateScreen(): PreferenceScreen { private fun populateScreen(): PreferenceScreen {
val sourceId = requireArguments().getLong(SOURCE_ID) val sourceId = requireArguments().getLong(SOURCE_ID)
val source = Injekt.get<SourceManager>().get(sourceId)!! as ConfigurableSource val source = Injekt.get<SourceManager>().getOrStub(sourceId)
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
if (source is ConfigurableSource) {
val dataStore = SharedPreferencesDataStore(source.sourcePreferences()) val dataStore = SharedPreferencesDataStore(source.sourcePreferences())
preferenceManager.preferenceDataStore = dataStore preferenceManager.preferenceDataStore = dataStore
val sourceScreen = preferenceManager.createPreferenceScreen(requireContext())
source.setupPreferenceScreen(sourceScreen) source.setupPreferenceScreen(sourceScreen)
sourceScreen.forEach { pref -> sourceScreen.forEach { pref ->
pref.isIconSpaceReserved = false pref.isIconSpaceReserved = false
@ -150,6 +150,7 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
} }
} }
} }
}
return sourceScreen return sourceScreen
} }
@ -158,9 +159,9 @@ class SourcePreferencesFragment : PreferenceFragmentCompat() {
private const val SOURCE_ID = "source_id" private const val SOURCE_ID = "source_id"
fun getInstance(sourceId: Long): SourcePreferencesFragment { fun getInstance(sourceId: Long): SourcePreferencesFragment {
val fragment = SourcePreferencesFragment() return SourcePreferencesFragment().apply {
fragment.arguments = bundleOf(SOURCE_ID to sourceId) arguments = bundleOf(SOURCE_ID to sourceId)
return fragment }
} }
} }
} }

View File

@ -74,7 +74,7 @@ class DeepLinkScreenModel(
} }
private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga { 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)) ?: networkToLocalManga.await(sManga.toDomainManga(sourceId))
} }

View File

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Badge import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox import androidx.compose.material3.BadgedBox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRailItem 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,
)
} }
} }

View File

@ -349,7 +349,7 @@ class MainActivity : BaseActivity() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (!preferences.shownOnboardingFlow().get()) { if (!preferences.shownOnboardingFlow().get() && navigator.lastItem !is OnboardingScreen) {
navigator.push(OnboardingScreen()) navigator.push(OnboardingScreen())
} }
} }

View File

@ -132,7 +132,7 @@ class MangaScreen(
) )
}.takeIf { isHttpSource }, }.takeIf { isHttpSource },
onTrackingClicked = { onTrackingClicked = {
if (successState.trackingCount == 0) { if (screenModel.loggedInTrackers.isEmpty()) {
navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking)) navigator.push(SettingsScreen(SettingsScreen.Destination.Tracking))
} else { } else {
screenModel.showTrackDialog() screenModel.showTrackDialog()

View File

@ -118,7 +118,7 @@ class MangaScreenModel(
private val successState: State.Success? private val successState: State.Success?
get() = state.value as? 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? val manga: Manga?
get() = successState?.manga get() = successState?.manga
@ -636,8 +636,9 @@ class MangaScreenModel(
) { ) {
val successState = successState ?: return val successState = successState ?: return
screenModelScope.launchNonCancellable {
if (startNow) { if (startNow) {
val chapterId = chapters.singleOrNull()?.id ?: return val chapterId = chapters.singleOrNull()?.id ?: return@launchNonCancellable
downloadManager.startDownloadNow(chapterId) downloadManager.startDownloadNow(chapterId)
} else { } else {
downloadChapters(chapters) downloadChapters(chapters)
@ -647,7 +648,6 @@ class MangaScreenModel(
updateSuccessState { state -> updateSuccessState { state ->
state.copy(hasPromptedToAddBefore = true) state.copy(hasPromptedToAddBefore = true)
} }
screenModelScope.launch {
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = context.stringResource(MR.strings.snack_add_to_library), message = context.stringResource(MR.strings.snack_add_to_library),
actionLabel = context.stringResource(MR.strings.action_add), actionLabel = context.stringResource(MR.strings.action_add),

View File

@ -1,15 +1,16 @@
package eu.kanade.tachiyomi.ui.more package eu.kanade.tachiyomi.ui.more
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.more.onboarding.OnboardingScreen import eu.kanade.presentation.more.onboarding.OnboardingScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.setting.SettingsScreen 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -20,18 +21,22 @@ class OnboardingScreen : Screen() {
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val basePreferences = remember { Injekt.get<BasePreferences>() } val basePreferences = remember { Injekt.get<BasePreferences>() }
val storagePreferences = remember { Injekt.get<StoragePreferences>() } val shownOnboardingFlow by basePreferences.shownOnboardingFlow().collectAsState()
val uiPreferences = remember { Injekt.get<UiPreferences>() }
val finishOnboarding = { val finishOnboarding: () -> Unit = {
basePreferences.shownOnboardingFlow().set(true) basePreferences.shownOnboardingFlow().set(true)
navigator.pop() navigator.pop()
} }
BackHandler(
enabled = !shownOnboardingFlow,
onBack = {
// Prevent exiting if onboarding hasn't been completed
},
)
OnboardingScreen( OnboardingScreen(
storagePreferences = storagePreferences, onComplete = finishOnboarding,
uiPreferences = uiPreferences,
onComplete = { finishOnboarding() },
onRestoreBackup = { onRestoreBackup = {
finishOnboarding() finishOnboarding()
navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage))

View File

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
import android.content.Context import android.content.Context
import android.net.Uri 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.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import okio.buffer import okio.buffer

View File

@ -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)
}
}
}

View File

@ -1,6 +1,6 @@
object AndroidConfig { object AndroidConfig {
const val compileSdk = 34 const val compileSdk = 34
const val minSdk = 23 const val minSdk = 23
const val targetSdk = 30 const val targetSdk = 34
const val ndk = "22.1.7171670" const val ndk = "26.1.10909125"
} }

View 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,
)
}

View File

@ -10,24 +10,24 @@ class TrackRepositoryImpl(
) : TrackRepository { ) : TrackRepository {
override suspend fun getTrackById(id: Long): Track? { 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> { override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
return handler.awaitList { return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack) manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
} }
} }
override fun getTracksAsFlow(): Flow<List<Track>> { override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList { return handler.subscribeToList {
manga_syncQueries.getTracks(::mapTrack) manga_syncQueries.getTracks(TrackMapper::mapTrack)
} }
} }
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> { override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList { 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,
)
} }

View File

@ -70,6 +70,10 @@ getAllManga:
SELECT * SELECT *
FROM mangas; FROM mangas;
getAllMangaSourceAndUrl:
SELECT source, url
FROM mangas;
getMangasWithFavoriteTimestamp: getMangasWithFavoriteTimestamp:
SELECT * SELECT *
FROM mangas FROM mangas

View File

@ -7,4 +7,13 @@ data class History(
val chapterId: Long, val chapterId: Long,
val readAt: Date?, val readAt: Date?,
val readDuration: Long, val readDuration: Long,
) ) {
companion object {
fun create() = History(
id = -1L,
chapterId = -1L,
readAt = null,
readDuration = -1L,
)
}
}

View File

@ -6,7 +6,7 @@ import tachiyomi.domain.manga.repository.MangaRepository
class GetMangaByUrlAndSourceId( class GetMangaByUrlAndSourceId(
private val mangaRepository: MangaRepository, 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) return mangaRepository.getMangaByUrlAndSourceId(url, sourceId)
} }
} }

View File

@ -6,8 +6,14 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.shareIn
class StorageManager( class StorageManager(
private val context: Context, private val context: Context,
@ -16,24 +22,33 @@ class StorageManager(
private val scope = CoroutineScope(Dispatchers.IO) 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 { init {
storagePreferences.baseStorageDirectory().changes() storagePreferences.baseStorageDirectory().changes()
.onEach { baseDir = getBaseDir(it) } .drop(1)
.launchIn(scope) .distinctUntilChanged()
} .onEach { uri ->
baseDir = getBaseDir(uri)
private fun getBaseDir(path: String): UniFile? { baseDir?.let { parent ->
val file = UniFile.fromUri(context, path.toUri())
return file.takeIf { it?.exists() == true }?.also { parent ->
parent.createDirectory(AUTOMATIC_BACKUPS_PATH) parent.createDirectory(AUTOMATIC_BACKUPS_PATH)
parent.createDirectory(LOCAL_SOURCE_PATH) parent.createDirectory(LOCAL_SOURCE_PATH)
parent.createDirectory(DOWNLOADS_PATH).also { parent.createDirectory(DOWNLOADS_PATH).also {
DiskUtil.createNoMediaFile(it, context) DiskUtil.createNoMediaFile(it, context)
} }
} }
_changes.send(Unit)
}
.launchIn(scope)
}
private fun getBaseDir(uri: String): UniFile? {
return UniFile.fromUri(context, uri.toUri())
.takeIf { it?.exists() == true }
} }
fun getAutomaticBackupsDirectory(): UniFile? { fun getAutomaticBackupsDirectory(): UniFile? {

View File

@ -23,5 +23,4 @@ org.gradle.caching=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
android.useAndroidX=true android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false

View File

@ -6,7 +6,7 @@ paging_version = "3.2.1"
[libraries] [libraries]
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" } 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" appcompat = "androidx.appcompat:appcompat:1.6.1"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" 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" benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.2"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.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] [bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]

View File

@ -1,10 +1,10 @@
[versions] [versions]
compiler = "1.5.6" compiler = "1.5.6"
compose-bom = "2023.12.00-alpha03" compose-bom = "2023.12.00-alpha04"
accompanist = "0.33.2-alpha" accompanist = "0.33.2-alpha"
[libraries] [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" } bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
foundation = { module = "androidx.compose.foundation:foundation" } foundation = { module = "androidx.compose.foundation:foundation" }
animation = { module = "androidx.compose.animation:animation" } animation = { module = "androidx.compose.animation:animation" }

View File

@ -1,13 +1,13 @@
[versions] [versions]
kotlin_version = "1.9.21" kotlin_version = "1.9.21"
serialization_version = "1.6.2" serialization_version = "1.6.2"
xml_serialization_version = "0.86.2" xml_serialization_version = "0.86.3"
[libraries] [libraries]
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", 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-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.3" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }

View File

@ -2,7 +2,7 @@
aboutlib_version = "10.9.2" aboutlib_version = "10.9.2"
leakcanary = "2.12" leakcanary = "2.12"
moko = "0.23.0" moko = "0.23.0"
okhttp_version = "5.0.0-alpha.11" okhttp_version = "5.0.0-alpha.12"
richtext = "0.17.0" richtext = "0.17.0"
shizuku_version = "12.2.0" shizuku_version = "12.2.0"
sqldelight = "2.0.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-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", 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" } 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" 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-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", 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" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
photoview = "com.github.chrisbanes:PhotoView:2.3.0" photoview = "com.github.chrisbanes:PhotoView:2.3.0"
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.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-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", 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-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" google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"

View File

@ -306,9 +306,9 @@
<string name="pref_category_security">الأمان و الخصوصية</string> <string name="pref_category_security">الأمان و الخصوصية</string>
<string name="pref_manage_notifications">أدر الإشعارات</string> <string name="pref_manage_notifications">أدر الإشعارات</string>
<string name="pref_date_format">صيغة التاريخ</string> <string name="pref_date_format">صيغة التاريخ</string>
<string name="theme_system">اتبع مظهر النظام</string> <string name="theme_system">النظام</string>
<string name="theme_dark">مفعّل</string> <string name="theme_dark">داكن</string>
<string name="theme_light">غير مفعّل</string> <string name="theme_light">فاتح</string>
<string name="battery_optimization_disabled">تم إلغاء وضع تحسين البطارية مُسبقاً</string> <string name="battery_optimization_disabled">تم إلغاء وضع تحسين البطارية مُسبقاً</string>
<string name="pref_disable_battery_optimization_summary">يساعد في عملية تحديث المكتبة والنسخ الإحتياطي في الخلفية</string> <string name="pref_disable_battery_optimization_summary">يساعد في عملية تحديث المكتبة والنسخ الإحتياطي في الخلفية</string>
<string name="pref_disable_battery_optimization">إطفاء وضع تحسين البطارية</string> <string name="pref_disable_battery_optimization">إطفاء وضع تحسين البطارية</string>
@ -768,4 +768,20 @@
<string name="action_bar_up_description">اصعد</string> <string name="action_bar_up_description">اصعد</string>
<string name="pref_storage_location">مكان التخزين</string> <string name="pref_storage_location">مكان التخزين</string>
<string name="pref_storage_location_info">يُستخدَم في الاحتياط وتنزيل الفصول والمصدر المحليِّ.</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> </resources>

View File

@ -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_info">Select a folder where %1$s will store chapter downloads, backups, and more.\n\nA dedicated folder is recommended.\n\nSelected folder: %2$s</string>
<string name="onboarding_storage_action_select">Select a folder</string> <string name="onboarding_storage_action_select">Select a folder</string>
<string name="onboarding_storage_selection_required">A folder must be selected</string> <string name="onboarding_storage_selection_required">A folder must be selected</string>
<string name="onboarding_permission_type_required">Required</string>
<string name="onboarding_permission_type_optional">Optional</string>
<string name="onboarding_permission_install_apps">Install apps permission</string>
<string name="onboarding_permission_install_apps_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_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> <string name="onboarding_guides_returning_user">Already used %s before?</string>

View File

@ -52,4 +52,16 @@
<item quantity="one">পরবর্তী অপঠিত অধ্যায়</item> <item quantity="one">পরবর্তী অপঠিত অধ্যায়</item>
<item quantity="other">পরবর্তী %d টি অপঠিত অধ্যায়</item> <item quantity="other">পরবর্তী %d টি অপঠিত অধ্যায়</item>
</plurals> </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> </resources>

View File

@ -615,4 +615,14 @@
<string name="action_not_now">এখন না</string> <string name="action_not_now">এখন না</string>
<string name="pref_debug_info">ডিবাগ তথ্য</string> <string name="pref_debug_info">ডিবাগ তথ্য</string>
<string name="skipped_reason_not_always_update">বাদ দেওয়া হয়েছে কারণ সিরিজের আপডেটের প্রয়োজন নেই৷</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> </resources>

View File

@ -301,9 +301,9 @@
<string name="action_sort_latest_chapter">Darrer capítol</string> <string name="action_sort_latest_chapter">Darrer capítol</string>
<string name="action_view_chapters">Mostra els capítols</string> <string name="action_view_chapters">Mostra els capítols</string>
<string name="action_cancel_all">Cancel·la-ho tot</string> <string name="action_cancel_all">Cancel·la-ho tot</string>
<string name="theme_light">Desactivat</string> <string name="theme_light">Clar</string>
<string name="theme_dark">Activat</string> <string name="theme_dark">Fosc</string>
<string name="theme_system">Per defecte del sistema</string> <string name="theme_system">Sistema</string>
<string name="pref_manage_notifications">Gestiona les notificacions</string> <string name="pref_manage_notifications">Gestiona les notificacions</string>
<string name="pref_category_security">Seguretat i privadesa</string> <string name="pref_category_security">Seguretat i privadesa</string>
<string name="lock_with_biometrics">Requereix desblocatge</string> <string name="lock_with_biometrics">Requereix desblocatge</string>
@ -768,4 +768,20 @@
<string name="pref_storage_location_info">Sutilitza per a les còpies de seguretat automàtiques, les baixades de capítols i la font local.</string> <string name="pref_storage_location_info">Sutilitza 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_menu_overflow_description">Més opcions</string>
<string name="action_bar_up_description">Navega cap amunt</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 sha definit una ubicació demmagatzematge</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> </resources>

View File

@ -301,9 +301,9 @@
<string name="action_sort_latest_chapter">Neuestes Kapitel</string> <string name="action_sort_latest_chapter">Neuestes Kapitel</string>
<string name="action_view_chapters">Kapitel anzeigen</string> <string name="action_view_chapters">Kapitel anzeigen</string>
<string name="action_cancel_all">Alle abbrechen</string> <string name="action_cancel_all">Alle abbrechen</string>
<string name="theme_light">Aus</string> <string name="theme_light">Hell</string>
<string name="theme_dark">An</string> <string name="theme_dark">Dunkel</string>
<string name="theme_system">Systemeinstellung</string> <string name="theme_system">System</string>
<string name="pref_manage_notifications">Benachrichtigungen verwalten</string> <string name="pref_manage_notifications">Benachrichtigungen verwalten</string>
<string name="pref_category_security">Sicherheit und Privatsphäre</string> <string name="pref_category_security">Sicherheit und Privatsphäre</string>
<string name="lock_with_biometrics">Entsperren erforderlich</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="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_menu_overflow_description">Weitere Optionen</string>
<string name="action_bar_up_description">Nach oben navigieren</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> </resources>

View File

@ -347,9 +347,9 @@
<string name="pref_category_security">Ασφάλεια και ιδιωτικότητα</string> <string name="pref_category_security">Ασφάλεια και ιδιωτικότητα</string>
<string name="pref_manage_notifications">Διαχείριση ειδοποιήσεων</string> <string name="pref_manage_notifications">Διαχείριση ειδοποιήσεων</string>
<string name="pref_date_format">Μορφή ημερομηνίας</string> <string name="pref_date_format">Μορφή ημερομηνίας</string>
<string name="theme_system">Ακολουθήστε το σύστημα</string> <string name="theme_system">Σύστημα</string>
<string name="theme_dark">Ενεργοποιημένο</string> <string name="theme_dark">Σκοτεινό</string>
<string name="theme_light">Απενεργοποιημένο</string> <string name="theme_light">Φωτεινό</string>
<string name="action_move_to_bottom">Μετακίνηση στον πάτο</string> <string name="action_move_to_bottom">Μετακίνηση στον πάτο</string>
<string name="action_move_to_top">Μετακίνηση στην κορυφή</string> <string name="action_move_to_top">Μετακίνηση στην κορυφή</string>
<string name="action_cancel_all">Ακύρωση όλων</string> <string name="action_cancel_all">Ακύρωση όλων</string>
@ -768,4 +768,20 @@
<string name="action_bar_up_description">Πλοήγηση προς τα πάνω</string> <string name="action_bar_up_description">Πλοήγηση προς τα πάνω</string>
<string name="pref_storage_location">Τοποθεσία αποθήκευσης</string> <string name="pref_storage_location">Τοποθεσία αποθήκευσης</string>
<string name="pref_storage_location_info">Χρησιμοποιείται για αυτόματα αντίγραφα ασφαλείας, λήψη κεφαλαίων και τοπική πηγή.</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> </resources>

View File

@ -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_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="pref_remove_after_read">Borrar capítulos terminados de forma automática</string>
<string name="services">Servicios de seguimiento</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="used_cache">Usado: %1$s</string>
<string name="cache_deleted">Se vació la caché. Se han eliminado %1$d archivos</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> <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_sort_latest_chapter">Por capítulo más reciente</string>
<string name="action_view_chapters">Ver capítulos</string> <string name="action_view_chapters">Ver capítulos</string>
<string name="action_cancel_all">Cancelar todo</string> <string name="action_cancel_all">Cancelar todo</string>
<string name="theme_light">No</string> <string name="theme_light">Claro</string>
<string name="theme_dark"></string> <string name="theme_dark">Oscuro</string>
<string name="theme_system">Según ajustes del sistema</string> <string name="theme_system">Sistema</string>
<string name="pref_manage_notifications">Gestionar notificaciones</string> <string name="pref_manage_notifications">Gestionar notificaciones</string>
<string name="pref_category_security">Seguridad y privacidad</string> <string name="pref_category_security">Seguridad y privacidad</string>
<string name="lock_with_biometrics">Requiere desbloqueo</string> <string name="lock_with_biometrics">Requiere desbloqueo</string>
@ -369,7 +369,7 @@
<string name="gray_background">Gris</string> <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="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="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="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="unofficial_extension_message">Esta extensión no es de la lista oficial de extensiones.</string>
<string name="ext_unofficial">No oficial</string> <string name="ext_unofficial">No oficial</string>
@ -383,7 +383,7 @@
<string name="action_migrate">Migrar</string> <string name="action_migrate">Migrar</string>
<string name="tabs_header">Pestañas</string> <string name="tabs_header">Pestañas</string>
<string name="action_display_show_tabs">Mostrar pestañas de categorí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_disable_all">Deshabilitar todo</string>
<string name="action_enable_all">Habilitar 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> <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> \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="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="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_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="pref_category_auto_download">Descarga automática</string>
<string name="rotation_landscape">En horizontal</string> <string name="rotation_landscape">En horizontal</string>
@ -476,7 +476,7 @@
<string name="include">Incluir: %s</string> <string name="include">Incluir: %s</string>
<string name="none">Ninguna</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="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_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="action_sort_chapter_fetch_date">Fecha de obtención del capítulo</string>
<string name="rotation_type">Tipo de rotación</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="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="ext_update_all">Actualizar todas</string>
<string name="channel_app_updates">Actualizaciones de la aplicación</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="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="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> <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">Borrar categoría</string>
<string name="delete_category_confirmation">¿Quieres borrar la categoría «%s»\?</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="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_user_agent_string">User agent predeterminado</string>
<string name="pref_reset_user_agent_string">Restablecer el nombre del navegador («user agent»)</string> <string name="pref_reset_user_agent_string">Restablecer user agent predeterminado</string>
<string name="action_remove_everything">Quitar todo</string> <string name="action_remove_everything">Quitar todo</string>
<string name="loader_rar5_error">La app no soporta el formato RARv5</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_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="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="update_already_running">Ya se está actualizando</string>
<string name="theme_tidalwave">Marea</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_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="download_ahead">Descargar por adelantado</string>
<string name="auto_download_while_reading">Descarga los capítulos siguientes mientras lees</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="unknown_title">Título desconocido</string>
<string name="invalid_location">Ubicación incorrecta: %s</string> <string name="invalid_location">Ubicación incorrecta: %s</string>
<string name="updates_last_update_info_just_now">Ahora mismo</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="download_notifier_cache_renewal">Reindexando descargas</string>
<string name="action_open_random_manga">Abrir un elemento al azar</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> <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="action_bar_up_description">Subir un nivel</string>
<string name="pref_storage_location">Ubicación del almacenamiento</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="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> </resources>

View File

@ -99,9 +99,9 @@
<string name="pref_manage_notifications">Pamahalaan ang mga abiso</string> <string name="pref_manage_notifications">Pamahalaan ang mga abiso</string>
<string name="pref_category_security">Seguridad at privacy</string> <string name="pref_category_security">Seguridad at privacy</string>
<string name="pref_date_format">Ayos ng petsa</string> <string name="pref_date_format">Ayos ng petsa</string>
<string name="theme_dark">Nakabukas</string> <string name="theme_dark">Madilim</string>
<string name="theme_light">Nakasara</string> <string name="theme_light">Maliwanag</string>
<string name="theme_system">Sundan ang sistema</string> <string name="theme_system">Sistema</string>
<string name="pref_category_about">Patungkol</string> <string name="pref_category_about">Patungkol</string>
<string name="pref_category_advanced">Karagdagan</string> <string name="pref_category_advanced">Karagdagan</string>
<string name="pref_category_tracking">Pagta-track</string> <string name="pref_category_tracking">Pagta-track</string>
@ -115,7 +115,7 @@
<string name="last_read_chapter">Huling nabasang kabanata</string> <string name="last_read_chapter">Huling nabasang kabanata</string>
<string name="disabled">Sarado</string> <string name="disabled">Sarado</string>
<string name="pref_remove_after_marked_as_read">Pagkamarkahang nabasa na</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_webtoon_side_padding">Kapal ng gilid</string>
<string name="pref_category_reading">Pagbabasa</string> <string name="pref_category_reading">Pagbabasa</string>
<string name="pref_always_show_chapter_transition">Ipakita palagi ang paglipat-kabanata</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_center">Gitna</string>
<string name="zoom_start_right">Kanan</string> <string name="zoom_start_right">Kanan</string>
<string name="zoom_start_left">Kaliwa</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="pref_zoom_start">Panimulang pag-zoom</string>
<string name="scale_type_smart_fit">Matalinong pagsasalaki</string> <string name="scale_type_smart_fit">Matalinong pagsasalaki</string>
<string name="scale_type_original_size">Orihinal na laki</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. \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 \n
\nTinatanggap mo ang mga bantang ito sa pagtiwala sa certificate na ito.</string> \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_uninstall">I-uninstall</string>
<string name="ext_trust">Tiwala</string> <string name="ext_trust">Tiwala</string>
<string name="ext_untrusted">Kaduda-duda</string> <string name="ext_untrusted">Kaduda-duda</string>
@ -202,17 +202,17 @@
<string name="default_category_summary">Palaging tanungin</string> <string name="default_category_summary">Palaging tanungin</string>
<string name="default_category">Default na kategorya</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_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="pref_update_only_non_completed">May \"Kumpleto\" na estado</string>
<string name="charging">Kapag naka-charge</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_weekly">Linggo-linggo</string>
<string name="update_48hour">Kada 2 araw</string> <string name="update_48hour">Kada 2 araw</string>
<string name="update_24hour">Araw-araw</string> <string name="update_24hour">Araw-araw</string>
<string name="update_12hour">Kada 12 oras</string> <string name="update_12hour">Kada 12 oras</string>
<string name="update_6hour">Kada 6 na oras</string> <string name="update_6hour">Kada 6 na oras</string>
<string name="update_never">Nakapatay</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="pref_category_library_update">Panlahatang update</string>
<string name="landscape">Pahiga</string> <string name="landscape">Pahiga</string>
<string name="portrait">Patayo</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_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="pref_refresh_library_covers">I-refresh ang mga cover sa aklatan</string>
<string name="clear_database_completed">Binura na</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_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="pref_clear_database">Linisin ang database</string>
<string name="cache_delete_error">Nagka-error habang nililinis</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="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="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="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="enhanced_services">Pinahusay na tracker</string>
<string name="theme_midnightdusk">Hatinggabi</string> <string name="theme_midnightdusk">Hatinggabi</string>
<string name="theme_greenapple">Berdeng Mansanas</string> <string name="theme_greenapple">Berdeng Mansanas</string>
@ -519,7 +519,7 @@
<string name="theme_yinyang">Yin at Yang</string> <string name="theme_yinyang">Yin at Yang</string>
<string name="theme_tako">Tako</string> <string name="theme_tako">Tako</string>
<string name="theme_strawberrydaiquiri">Presas</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_lowest">Pinakamababa</string>
<string name="pref_low">Mababa</string> <string name="pref_low">Mababa</string>
<string name="pref_high">Mataas</string> <string name="pref_high">Mataas</string>
@ -535,9 +535,9 @@
<string name="getting_started_guide">Gabay sa Pagsisimula</string> <string name="getting_started_guide">Gabay sa Pagsisimula</string>
<string name="pref_tablet_ui_mode">Pang-tablet na UI</string> <string name="pref_tablet_ui_mode">Pang-tablet na UI</string>
<string name="help_translate">Tumulong sa pagsalin</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_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_shizuku_stopped">Di tumatakbo ang Shizuku</string>
<string name="ext_installer_legacy">Legasiya</string> <string name="ext_installer_legacy">Legasiya</string>
<string name="ext_installer_pref">Taga-install</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="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="connected_to_wifi">Sa Wi-Fi lang</string>
<string name="update_72hour">Kada 3 araw</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="channel_app_updates">Mga update sa app</string>
<string name="ext_update_all">I-update lahat</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> <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="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_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="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="pref_navigate_pan">I-pan ang mga malalapad na larawan</string>
<string name="learn_more">Matuto pa</string> <string name="learn_more">Matuto pa</string>
<string name="channel_skipped">Nilaktawan</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="skipped_reason_not_always_update">Nilaktawan dahil hindi kailangan ang pag-update sa serye</string>
<string name="action_search_hint">Maghanap…</string> <string name="action_search_hint">Maghanap…</string>
<string name="pref_reader_summary">Paraan ng pagbasa, pagpapakita, nabigasyon</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_tracking_summary">Isahang pagsabay sa progress, pinahusay na pagsabay</string>
<string name="pref_appearance_summary">Tema, ayos ng petsa &amp; oras</string> <string name="pref_appearance_summary">Tema, ayos ng petsa &amp; oras</string>
<string name="pref_backup_summary">Mano-mano at awtomatikong pag-backup, espasyo sa storage</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="selected">Napili</string>
<string name="not_selected">Di napili</string> <string name="not_selected">Di napili</string>
<string name="action_bar_up_description">Mag-navigate pataas</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> </resources>

View File

@ -296,9 +296,9 @@
<string name="action_sort_latest_chapter">最新章の更新順</string> <string name="action_sort_latest_chapter">最新章の更新順</string>
<string name="action_view_chapters">章を見る</string> <string name="action_view_chapters">章を見る</string>
<string name="action_cancel_all">すべてキャンセル</string> <string name="action_cancel_all">すべてキャンセル</string>
<string name="theme_light">オフ</string> <string name="theme_light">ライト</string>
<string name="theme_dark">オン</string> <string name="theme_dark">ダーク</string>
<string name="theme_system">システムに従う</string> <string name="theme_system">システム</string>
<string name="pref_manage_notifications">通知設定</string> <string name="pref_manage_notifications">通知設定</string>
<string name="pref_category_security">セキュリティとプライバシー</string> <string name="pref_category_security">セキュリティとプライバシー</string>
<string name="lock_with_biometrics">アンロックを必要とする</string> <string name="lock_with_biometrics">アンロックを必要とする</string>
@ -768,4 +768,29 @@
<string name="action_bar_up_description">上に移動</string> <string name="action_bar_up_description">上に移動</string>
<string name="pref_storage_location">保存場所</string> <string name="pref_storage_location">保存場所</string>
<string name="pref_storage_location_info">自動バックアップ、章のダウンロード、ローカル ソースの保存位置となります。</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> </resources>

View File

@ -150,7 +150,7 @@
<string name="cookies_cleared">쿠키 삭제됨</string> <string name="cookies_cleared">쿠키 삭제됨</string>
<string name="pref_clear_database">데이터베이스 삭제</string> <string name="pref_clear_database">데이터베이스 삭제</string>
<string name="pref_clear_database_summary">서재에 추가되지 않은 항목의 기록을 삭제합니다</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="version">버전</string>
<string name="pref_enable_acra">오류 보고서 전송</string> <string name="pref_enable_acra">오류 보고서 전송</string>
<string name="pref_acra_summary">버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다</string> <string name="pref_acra_summary">버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다</string>
@ -309,7 +309,7 @@
<string name="pref_disable_battery_optimization">배터리 최적화 끄기</string> <string name="pref_disable_battery_optimization">배터리 최적화 끄기</string>
<string name="restore_miui_warning">MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다.</string> <string name="restore_miui_warning">MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다.</string>
<string name="restore_in_progress">복원이 이미 진행중 입니다</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="pref_dns_over_https">DNS over HTTPS (DoH)</string>
<string name="label_data">데이터</string> <string name="label_data">데이터</string>
<string name="backup_in_progress">백업이 이미 진행중입니다</string> <string name="backup_in_progress">백업이 이미 진행중입니다</string>
@ -448,7 +448,7 @@
<string name="backup_restore_missing_sources">없어진 소스:</string> <string name="backup_restore_missing_sources">없어진 소스:</string>
<string name="backup_restore_missing_trackers">로그인 되지않은 트래커:</string> <string name="backup_restore_missing_trackers">로그인 되지않은 트래커:</string>
<string name="pref_auto_clear_chapter_cache">앱 실행 시 회차 캐시 삭제</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="about_dont_kill_my_app">일부 제조사는 백그라운드 서비스를 종료하는 추가적인 제한 사항이 있습니다. 자세한 사항은 웹사이트를 참조하세요.</string>
<string name="pref_tablet_ui_mode">태블릿 UI</string> <string name="pref_tablet_ui_mode">태블릿 UI</string>
<string name="tabs_header"></string> <string name="tabs_header"></string>
@ -622,7 +622,7 @@
<string name="cant_open_last_read_chapter">마지막 회차를 열 수 없습니다</string> <string name="cant_open_last_read_chapter">마지막 회차를 열 수 없습니다</string>
<string name="appwidget_updates_description">최근에 업데이트된 항목 보기</string> <string name="appwidget_updates_description">최근에 업데이트된 항목 보기</string>
<string name="on_hold_list">보류 목록</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="loader_rar5_error">RARv5 포맷은 지원되지 않습니다</string>
<string name="appwidget_unavailable_locked">앱 잠금 사용 중에는 위젯을 이용할 수 없습니다</string> <string name="appwidget_unavailable_locked">앱 잠금 사용 중에는 위젯을 이용할 수 없습니다</string>
<string name="theme_tidalwave">파도</string> <string name="theme_tidalwave">파도</string>

View File

@ -300,8 +300,8 @@
<string name="label_more">Mer</string> <string name="label_more">Mer</string>
<string name="action_view_chapters">Vis kapitler</string> <string name="action_view_chapters">Vis kapitler</string>
<string name="action_cancel_all">Avbryt alle</string> <string name="action_cancel_all">Avbryt alle</string>
<string name="theme_light">Av</string> <string name="theme_light">Lyst</string>
<string name="theme_dark"></string> <string name="theme_dark">Mørkt</string>
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="pref_manage_notifications">Håndter merknader</string> <string name="pref_manage_notifications">Håndter merknader</string>
<string name="pref_category_security">Sikkerhet og personvern</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="label_data_storage">Data og lagring</string>
<string name="file_null_uri_error">Ingen fil valgt</string> <string name="file_null_uri_error">Ingen fil valgt</string>
<string name="exclude_scanlators">Ekskluder skanningsoversettere</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> </resources>

View File

@ -783,4 +783,14 @@
\nUma pasta dedicada é recomendada. \nUma pasta dedicada é recomendada.
\n \n
\nPasta selecionada: %2$s</string> \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> </resources>

View File

@ -301,8 +301,8 @@
<string name="action_sort_latest_chapter">Последняя глава</string> <string name="action_sort_latest_chapter">Последняя глава</string>
<string name="action_view_chapters">Просмотреть главы</string> <string name="action_view_chapters">Просмотреть главы</string>
<string name="action_cancel_all">Отменить всё</string> <string name="action_cancel_all">Отменить всё</string>
<string name="theme_light">Выключен</string> <string name="theme_light">Светлая</string>
<string name="theme_dark">Включён</string> <string name="theme_dark">Тёмная</string>
<string name="theme_system">Система</string> <string name="theme_system">Система</string>
<string name="pref_manage_notifications">Управление уведомлениями</string> <string name="pref_manage_notifications">Управление уведомлениями</string>
<string name="pref_category_security">Безопасность и конфиденциальность</string> <string name="pref_category_security">Безопасность и конфиденциальность</string>
@ -768,4 +768,20 @@
<string name="action_bar_up_description">Перейти вверх</string> <string name="action_bar_up_description">Перейти вверх</string>
<string name="pref_storage_location">Путь хранилища</string> <string name="pref_storage_location">Путь хранилища</string>
<string name="pref_storage_location_info">Используется для автоматических резевных копии, загрузок глав и источнике на устройстве.</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> </resources>

View File

@ -26,12 +26,12 @@
<string name="action_webview_refresh">Rifresko</string> <string name="action_webview_refresh">Rifresko</string>
<string name="app_not_available">Aplikacioni i padisponueshem</string> <string name="app_not_available">Aplikacioni i padisponueshem</string>
<string name="pref_downloads_summary">Shkarkim automatik, shkarko përpara</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_greenapple">Molle jeshile</string>
<string name="theme_lavender">Livando</string> <string name="theme_lavender">Livando</string>
<string name="theme_yinyang">Yin &amp; Yang</string> <string name="theme_yinyang">Yin &amp; Yang</string>
<string name="theme_yotsuba">Yotsuba</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_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_manage_notifications">Menaxho njoftimet</string>
<string name="pref_app_language">Gjuha e aplikacionit</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="pref_show_nsfw_source">Shfaq në listat e burimeve dhe shtesave</string>
<string name="relative_time_today">Sot</string> <string name="relative_time_today">Sot</string>
<string name="pref_category_display">Shfaqja</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="pref_category_library_update">Përditësim global</string>
<string name="update_never">Joaktiv</string> <string name="update_never">Joaktiv</string>
<string name="update_12hour">Çdo 12 orë</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="connected_to_wifi">Vetëm në Wi-Fi</string>
<string name="network_not_metered">Vetëm në rrjet pa matje</string> <string name="network_not_metered">Vetëm në rrjet pa matje</string>
<string name="charging">Gjatë karikimit</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_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="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="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="all">Të gjitha</string>
<string name="none">Asnje</string> <string name="none">Asnje</string>
<string name="include">Përfshi: %s</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="download_notifier_text_only_wifi">Nuk ofrohet lidhje Wi-Fi</string>
<string name="information_empty_library">Biblioteka juaj është bosh</string> <string name="information_empty_library">Biblioteka juaj është bosh</string>
<string name="getting_started_guide">Udhëzues për fillimin</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_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_webview_required">WebView kërkohet për Tachiyomi</string>
<string name="information_cloudflare_bypass_failure">Dështoi të anashkalojë Cloudflare</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="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="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="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_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_filter_empty">Hiq filtrin</string>
<string name="action_sort_alpha">Sipas alfabetit</string> <string name="action_sort_alpha">Sipas alfabetit</string>
<string name="action_sort_total">Totali i kapitujve</string> <string name="action_sort_total">Totali i kapitujve</string>
<string name="label_warning">Paralajmërim</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="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_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_order_by_chapter_number">Sipas numrit të kapitullit</string>
<string name="action_display_download_badge">Kapitujt e shkarkuar</string> <string name="action_display_download_badge">Kapitujt e shkarkuar</string>
<string name="action_display_local_badge">Burimi lokal</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_library">Biblioteka</string>
<string name="pref_category_reader">Lexues</string> <string name="pref_category_reader">Lexues</string>
<string name="pref_category_downloads">Shkarkimet</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_browse_summary">Burimet, zgjerimet, kërkimi global</string>
<string name="pref_backup_summary">Rezervime manuale &amp; automatike</string> <string name="pref_backup_summary">Rezervime manuale &amp; automatike</string>
<string name="pref_category_advanced">E avancuar</string> <string name="pref_category_advanced">E avancuar</string>
@ -158,8 +158,8 @@
<string name="pref_category_about">Rreth</string> <string name="pref_category_about">Rreth</string>
<string name="pref_advanced_summary">Hidh regjistrat e përplasjeve, optimizimet e baterisë</string> <string name="pref_advanced_summary">Hidh regjistrat e përplasjeve, optimizimet e baterisë</string>
<string name="pref_category_theme">Tema</string> <string name="pref_category_theme">Tema</string>
<string name="theme_system">Ndiq sistemin</string> <string name="theme_system">Ndiq Sistemin</string>
<string name="theme_light">Joaktiv</string> <string name="theme_light">Ndrçim</string>
<string name="theme_strawberrydaiquiri">Daiquiri luleshtrydhe</string> <string name="theme_strawberrydaiquiri">Daiquiri luleshtrydhe</string>
<string name="pref_app_theme">Tema e aplikacionit</string> <string name="pref_app_theme">Tema e aplikacionit</string>
<string name="theme_monet">Dinamik</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_update_all">Përditëso të gjitha</string>
<string name="ext_updates_pending">Përditësimet në pritje</string> <string name="ext_updates_pending">Përditësimet në pritje</string>
<string name="on">Aktiv</string> <string name="on">Aktiv</string>
<string name="off">fikur</string> <string name="off">Fikur</string>
<string name="categories">Kategoritë</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="chapters">Kapituj</string>
<string name="track">Ndjekja</string> <string name="track">Gjurmimi</string>
<string name="history">Historia</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_last_manga_update">Kontrolli i përditësimit të fundit</string>
<string name="action_sort_unread_count">Numër i palexuar</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">Ekrani</string>
<string name="action_display_mode">Modaliteti i ekranit</string> <string name="action_display_mode">Modaliteti i ekranit</string>
<string name="action_display_grid">Rrjetë kompakte</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="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="appwidget_unavailable_locked">Miniaplikacioni nuk ofrohet kur kyçja e aplikacionit është aktivizuar</string>
<string name="ext_pending">Në pritje</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="auto_download_while_reading">Shkarkim automatik gjatë leximit</string>
<string name="save_chapter_as_cbz">Ruaje si arkiv CBZ</string> <string name="save_chapter_as_cbz">Ruaje si arkiv CBZ</string>
<string name="tracking_guide">Udhëzues gjurmimi</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="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="black_background">E zezë</string>
<string name="pref_viewer_type">Modaliteti i parazgjedhur i leximit</string> <string name="pref_viewer_type">Modaliteti i parazgjedhur i leximit</string>
<string name="l_nav">Në formë L</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="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="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="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="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="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="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="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="action_filter_bookmarked">Faqeshënuar</string>
<string name="webtoon_viewer">Webtoon</string> <string name="webtoon_viewer">Webtoon</string>
<string name="disabled">I çaktivizuar</string> <string name="disabled">I çaktivizuar</string>
<string name="kindlish_nav">Kindle-ish</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="action_track">Pista</string>
<string name="pref_backup_interval">Frekuenca rezervë</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_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="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> <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="sort_by_upload_date">Sipas datës së ngarkimit</string>
<string name="manga_download">Shkarko</string> <string name="manga_download">Shkarko</string>
<string name="unread">Të palexuara</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="unfinished_list">Lista e papërfunduar</string>
<string name="on_hold_list">Në listën e pritjes</string> <string name="on_hold_list">Në listën e pritjes</string>
<string name="track_type">Lloji</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="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="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="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="pref_disable_battery_optimization">Çaktivizo optimizimin e baterisë</string>
<string name="email">Adresa e emailit</string> <string name="email">Adresa e emailit</string>
<string name="login_title">Hyni në %1$s</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_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_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="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="no_pinned_sources">Nuk ke burime të gozhduara</string>
<string name="local_filter_order_by">Urdhër nga</string> <string name="local_filter_order_by">Urdhër nga</string>
<string name="date">Data</string> <string name="date">Data</string>
@ -506,7 +506,7 @@
<string name="creating_backup">Duke krijuar rezervë</string> <string name="creating_backup">Duke krijuar rezervë</string>
<string name="creating_backup_error">Rezervimi dështoi</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="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="restore_in_progress">Rivendosja është tashmë në proces</string>
<string name="restoring_backup">Rivendosja e rezervës</string> <string name="restoring_backup">Rivendosja e rezervës</string>
<string name="restoring_backup_error">Rivendosja e rezervimit dështoi</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="label_data">Të dhënat</string>
<string name="used_cache">Përdorur: %1$s</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="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_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_refresh_library_covers">Rifresko kopertinat e bibliotekës</string>
<string name="pref_reset_viewer_flags">Rivendos cilësimet e lexuesit për seri</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="label_downloaded_only">Vetëm të shkarkuarat</string>
<string name="pref_incognito_mode">Modaliteti i fshehtë</string> <string name="pref_incognito_mode">Modaliteti i fshehtë</string>
<string name="pref_incognito_mode_summary">Ndalon leximin e historisë</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_title">Dil nga %1$s\?</string>
<string name="logout_success">Tani keni dalë nga llogaria</string> <string name="logout_success">Tani keni dalë nga llogaria</string>
<string name="unknown_error">Gabim i panjohur</string> <string name="unknown_error">Gabim i panjohur</string>
@ -578,7 +578,7 @@
<string name="cover_saved">Kopertina u ruajt</string> <string name="cover_saved">Kopertina u ruajt</string>
<string name="error_sharing_cover">Gabim në ndarjen e kopertinës</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="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="set_chapter_settings_as_default">Vendose si parësore</string>
<string name="no_chapters_error">Nuk u gjet asnjë kapitull</string> <string name="no_chapters_error">Nuk u gjet asnjë kapitull</string>
<string name="are_you_sure">A je i sigurt\?</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_started_reading_date">Data e fillimit</string>
<string name="track_status">Status</string> <string name="track_status">Status</string>
<string name="track_finished_reading_date">Data e mbarimit</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="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="snack_categories_deleted">Kategoritë u fshinë</string>
<string name="picture_saved">Fotografia u ruajt</string> <string name="picture_saved">Fotografia u ruajt</string>
@ -641,10 +641,10 @@
\n \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> \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">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_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_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="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="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> <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="label_read_chapters">Lexo</string>
<string name="not_applicable">N/A</string> <string name="not_applicable">N/A</string>
<string name="day_short">%dd</string> <string name="day_short">%dd</string>
<string name="label_tracker_section">Ndjekësit</string> <string name="label_tracker_section">Gjurmuesit</string>
<string name="label_tracked_titles">Hyrjet e ndjekura</string> <string name="label_tracked_titles">Elementet e gjurmuara</string>
<string name="action_not_now">Jo tani</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_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="label_titles_in_global_update">Ne përditësimin global</string>
<string name="hour_short">%do</string> <string name="hour_short">%do</string>
<string name="minute_short">%dm</string> <string name="minute_short">%dm</string>
<string name="information_no_manga_category">Kategorija është bosh</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="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_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="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. <string name="confirm_add_duplicate_manga">Ju keni një hyrje në librarni me të njëjtin emër.
\n \n
@ -700,4 +700,13 @@
<string name="track_error">%1$s gabim: %2$s</string> <string name="track_error">%1$s gabim: %2$s</string>
<string name="information_required_plain">*kërkohet</string> <string name="information_required_plain">*kërkohet</string>
<string name="copied_to_clipboard_plain">U kopjua në clipboard</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> </resources>

View File

@ -301,9 +301,9 @@
<string name="action_sort_latest_chapter">Senaste kapitel</string> <string name="action_sort_latest_chapter">Senaste kapitel</string>
<string name="action_view_chapters">Visa kapitel</string> <string name="action_view_chapters">Visa kapitel</string>
<string name="action_cancel_all">Avbryt alla</string> <string name="action_cancel_all">Avbryt alla</string>
<string name="theme_light">Av</string> <string name="theme_light">Ljus</string>
<string name="theme_dark"></string> <string name="theme_dark">Mörk</string>
<string name="theme_system">Följ systemet</string> <string name="theme_system">System</string>
<string name="pref_manage_notifications">Hantera aviseringar</string> <string name="pref_manage_notifications">Hantera aviseringar</string>
<string name="pref_category_security">Säkerhet och integritet</string> <string name="pref_category_security">Säkerhet och integritet</string>
<string name="lock_with_biometrics">Kräver upplåsning</string> <string name="lock_with_biometrics">Kräver upplåsning</string>
@ -768,4 +768,29 @@
<string name="selected">Vald</string> <string name="selected">Vald</string>
<string name="not_selected">Inte vald</string> <string name="not_selected">Inte vald</string>
<string name="action_bar_up_description">Navigera upp</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> </resources>

View File

@ -301,9 +301,9 @@
<string name="action_sort_latest_chapter">Son bölüm</string> <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_view_chapters">Bölümleri görüntüle</string>
<string name="action_cancel_all">Hepsini iptal et</string> <string name="action_cancel_all">Hepsini iptal et</string>
<string name="theme_light">Kapalı</string> <string name="theme_light">ık</string>
<string name="theme_dark">ık</string> <string name="theme_dark">Koyu</string>
<string name="theme_system">Sisteme uy</string> <string name="theme_system">Sistem</string>
<string name="pref_manage_notifications">Bildirimleri yönet</string> <string name="pref_manage_notifications">Bildirimleri yönet</string>
<string name="pref_category_security">Güvenlik ve gizlilik</string> <string name="pref_category_security">Güvenlik ve gizlilik</string>
<string name="lock_with_biometrics">Kilit açma gerektirir</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">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="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="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> </resources>

View File

@ -45,4 +45,7 @@
<plurals name="missing_chapters"> <plurals name="missing_chapters">
<item quantity="other">Đang thiếu %1$s</item> <item quantity="other">Đang thiếu %1$s</item>
</plurals> </plurals>
<plurals name="day">
<item quantity="other">%d ngày</item>
</plurals>
</resources> </resources>

View File

@ -315,14 +315,14 @@
<string name="pref_category_security">Bảo mật và quyền riêng tư</string> <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_manage_notifications">Quản lý thông báo</string>
<string name="pref_date_format">Định dạng ngày</string> <string name="pref_date_format">Định dạng ngày</string>
<string name="theme_system">Theo hệ thống</string> <string name="theme_system">Hệ thống</string>
<string name="theme_dark">Bật</string> <string name="theme_dark">Tối</string>
<string name="theme_light">Tắt</string> <string name="theme_light">Sáng</string>
<string name="pref_category_library">Thư viện</string> <string name="pref_category_library">Thư viện</string>
<string name="action_webview_refresh">Làm mới</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_forward">Chuyển tới trước</string>
<string name="action_webview_back">Trở lại</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_move_to_top">Di chuyển lên đầu</string>
<string name="action_oldest">Cũ nhất</string> <string name="action_oldest">Cũ nhất</string>
<string name="action_newest">Mới 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="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="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="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="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="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> <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="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="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="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_lowest">Thấp nhất</string>
<string name="pref_low">Thấp</string> <string name="pref_low">Thấp</string>
<string name="pref_high">Cao</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="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="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">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="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="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> <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">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_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_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="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_set_interval">Đặt khoảng thời gian</string>
<string name="action_ok">OK</string> <string name="action_ok">OK</string>
@ -727,4 +727,70 @@
<string name="unlock_app_title">Mở khoá %s</string> <string name="unlock_app_title">Mở khoá %s</string>
<string name="source_settings">Cài đặt nguồn</string> <string name="source_settings">Cài đặt nguồn</string>
<string name="app_settings">Cài đặt ứng dụng</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> </resources>

View File

@ -762,4 +762,7 @@
<string name="scanlator">扫译者</string> <string name="scanlator">扫译者</string>
<string name="action_sort_tracker_score">记录平台评分</string> <string name="action_sort_tracker_score">记录平台评分</string>
<string name="exclude_scanlators">排除的扫译者</string> <string name="exclude_scanlators">排除的扫译者</string>
<string name="action_menu_overflow_description">更多选项</string>
<string name="selected">已选择</string>
<string name="not_selected">未选择</string>
</resources> </resources>

View File

@ -291,9 +291,9 @@
<string name="pref_category_library">書櫃</string> <string name="pref_category_library">書櫃</string>
<string name="ext_obsolete">過舊</string> <string name="ext_obsolete">過舊</string>
<string name="obsolete_extension_message">這個擴充套件已無法使用,其可能無法正確運作或導致本程式發生問題。建議解除安裝。</string> <string name="obsolete_extension_message">這個擴充套件已無法使用,其可能無法正確運作或導致本程式發生問題。建議解除安裝。</string>
<string name="theme_light">關閉</string> <string name="theme_light">淺色</string>
<string name="theme_system">遵循系統</string> <string name="theme_system">系統</string>
<string name="theme_dark">開啟</string> <string name="theme_dark">深色</string>
<string name="pref_date_format">日期格式</string> <string name="pref_date_format">日期格式</string>
<string name="downloaded_only_summary">將套用至你書櫃中的作品</string> <string name="downloaded_only_summary">將套用至你書櫃中的作品</string>
<string name="label_downloaded_only">僅限下載內容</string> <string name="label_downloaded_only">僅限下載內容</string>
@ -643,7 +643,7 @@
<string name="pref_appearance_summary">主題、日期格式</string> <string name="pref_appearance_summary">主題、日期格式</string>
<string name="pref_downloads_summary">自動下載、預先下載</string> <string name="pref_downloads_summary">自動下載、預先下載</string>
<string name="pref_tracking_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_security_summary">上鎖應用程式、防窺畫面</string>
<string name="pref_advanced_summary">傾印當機記錄、電池效能最佳化</string> <string name="pref_advanced_summary">傾印當機記錄、電池效能最佳化</string>
<string name="crash_screen_restart_application">重新啟動應用程式</string> <string name="crash_screen_restart_application">重新啟動應用程式</string>
@ -750,9 +750,9 @@
<string name="source_settings">來源設定</string> <string name="source_settings">來源設定</string>
<string name="file_null_uri_error">未選擇檔案</string> <string name="file_null_uri_error">未選擇檔案</string>
<string name="relative_time_span_never">永不</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="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="label_data_storage">資料與儲存空間</string>
<string name="pref_storage_usage">儲存空間使用情形</string> <string name="pref_storage_usage">儲存空間使用情形</string>
<string name="action_sort_tracker_score">歷程平台評分</string> <string name="action_sort_tracker_score">歷程平台評分</string>
@ -768,4 +768,29 @@
<string name="action_bar_up_description">向上瀏覽</string> <string name="action_bar_up_description">向上瀏覽</string>
<string name="pref_storage_location">儲存位置</string> <string name="pref_storage_location">儲存位置</string>
<string name="pref_storage_location_info">供自動備份、章節下載和本機來源使用。</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> </resources>

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Newspaper import androidx.compose.material.icons.outlined.Newspaper
import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarDefaults
@ -38,6 +39,7 @@ fun InfoScreen(
subtitleText: String, subtitleText: String,
acceptText: String, acceptText: String,
onAcceptClick: () -> Unit, onAcceptClick: () -> Unit,
canAccept: Boolean = true,
rejectText: String? = null, rejectText: String? = null,
onRejectClick: (() -> Unit)? = null, onRejectClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
@ -63,8 +65,9 @@ fun InfoScreen(
vertical = MaterialTheme.padding.small, vertical = MaterialTheme.padding.small,
), ),
) { ) {
androidx.compose.material3.Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = canAccept,
onClick = onAcceptClick, onClick = onAcceptClick,
) { ) {
Text(text = acceptText) Text(text = acceptText)

View File

@ -1,6 +1,5 @@
package tachiyomi.presentation.core.util package tachiyomi.presentation.core.util
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -16,6 +15,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha 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.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged 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) { fun Modifier.selectedBackground(isSelected: Boolean): Modifier = if (isSelected) {
composed { composed {
val alpha = if (isSystemInDarkTheme()) 0.16f else 0.22f 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 { } else {
this this