Merge branch 'master' into sync-part-final

This commit is contained in:
KaiserBh 2023-08-13 19:19:47 +10:00 committed by GitHub
commit bedfbf3f71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 2767 additions and 2207 deletions

View File

@ -78,6 +78,7 @@ android {
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release")
isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark"
}

View File

@ -8,6 +8,7 @@
-keep,allowoptimization class kotlin.** { public protected *; }
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
-keep,allowoptimization class kotlin.time.** { public protected *; }
-keep,allowoptimization class okhttp3.** { public protected *; }
-keep,allowoptimization class okio.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; }

View File

@ -44,11 +44,6 @@
android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi">
<!-- enable profiling by macrobenchmark -->
<profileable
android:shell="true"
tools:targetApi="q" />
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTop"
@ -124,8 +119,8 @@
android:exported="false" />
<activity
android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist"
android:name=".ui.setting.track.TrackLoginActivity"
android:label="@string/track_activity_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@ -133,54 +128,12 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anilist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:host="anilist-auth"/>
<data android:host="bangumi-auth"/>
<data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="myanimelist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="shikimori-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="bangumi-auth"
android:scheme="tachiyomi" />
<data android:scheme="tachiyomi"/>
</intent-filter>
</activity>
<activity
@ -224,7 +177,8 @@
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service android:name=".extension.util.ExtensionInstallService"
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false" />
<service

File diff suppressed because it is too large Load Diff

View File

@ -24,5 +24,6 @@ class BasePreferences(
LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU(R.string.ext_installer_shizuku),
PRIVATE(R.string.ext_installer_private),
}
}

View File

@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HelpOutline
@ -176,7 +174,8 @@ private fun ExtensionDetails(
data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this)
}
},
Unit
}.takeIf { extension.isShared },
onClickAgeRating = {
showNsfwWarning = true
},
@ -209,7 +208,7 @@ private fun DetailsHeader(
extension: Extension,
onClickAgeRating: () -> Unit,
onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
onClickAppInfo: (() -> Unit)?,
) {
val context = LocalContext.current
@ -293,6 +292,7 @@ private fun DetailsHeader(
top = MaterialTheme.padding.small,
bottom = MaterialTheme.padding.medium,
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedButton(
modifier = Modifier.weight(1f),
@ -301,8 +301,7 @@ private fun DetailsHeader(
Text(stringResource(R.string.ext_uninstall))
}
Spacer(Modifier.width(16.dp))
if (onClickAppInfo != null) {
Button(
modifier = Modifier.weight(1f),
onClick = onClickAppInfo,
@ -313,6 +312,7 @@ private fun DetailsHeader(
)
}
}
}
HorizontalDivider()
}

View File

@ -1,6 +1,5 @@
package eu.kanade.presentation.browse.components
import android.content.pm.PackageManager
import android.util.DisplayMetrics
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
@ -31,6 +30,7 @@ import eu.kanade.domain.source.model.icon
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.source.model.Source
import tachiyomi.source.local.isLocal
@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
withIOContext {
value = try {
val appInfo = context.packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
val appResources = context.packageManager.getResourcesForApplication(appInfo)
Result.Success(
appResources.getDrawableForDensity(appInfo.icon, density, null)!!

View File

@ -0,0 +1,76 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun BottomReaderBar(
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
onClickOrientationMode: () -> Unit,
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
Row(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = onClickReadingMode) {
Icon(
painter = painterResource(readingMode.iconRes),
contentDescription = stringResource(R.string.viewer),
)
}
IconButton(onClick = onClickCropBorder) {
Icon(
painter = painterResource(if (cropEnabled) R.drawable.ic_crop_24dp else R.drawable.ic_crop_off_24dp),
contentDescription = stringResource(R.string.pref_crop_borders),
)
}
IconButton(onClick = onClickOrientationMode) {
Icon(
painter = painterResource(orientationMode.iconRes),
contentDescription = stringResource(R.string.pref_rotation_type),
)
}
IconButton(onClick = onClickSettings) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.action_settings),
)
}
}
}

View File

@ -53,14 +53,6 @@ fun ChapterNavigator(
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
val haptic = LocalHapticFeedback.current
// We explicitly handle direction based on the reader viewer rather than the system direction
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
@ -69,6 +61,15 @@ fun ChapterNavigator(
containerColor = backgroundColor,
disabledContainerColor = backgroundColor,
)
// We explicitly handle direction based on the reader viewer rather than the system direction
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
FilledIconButton(
enabled = if (isRtl) enabledNext else enabledPrevious,
onClick = if (isRtl) onNextChapter else onPreviousChapter,

View File

@ -0,0 +1,56 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.orientationType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.material.padding
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
@Composable
fun OrientationModeSelectDialog(
onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel,
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
SettingsChipRow(R.string.rotation_type) {
orientationTypeOptions.map { (stringRes, it) ->
FilterChip(
selected = it == orientationType,
onClick = {
screenModel.onChangeOrientation(it)
onChange(stringRes)
},
label = { Text(stringResource(stringRes)) },
)
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.reader
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View File

@ -0,0 +1,56 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.manga.model.readingModeType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.material.padding
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
@Composable
fun ReadingModeSelectDialog(
onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel,
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
SettingsChipRow(R.string.pref_category_reading_mode) {
readingModeOptions.map { (stringRes, it) ->
FilterChip(
selected = it == readingMode,
onClick = {
screenModel.onChangeReadingMode(it)
onChange(stringRes)
},
label = { Text(stringResource(stringRes)) },
)
}
}
}
}
}

View File

@ -42,11 +42,12 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
pref = screenModel.preferences.fullscreen(),
)
// TODO: hide if there's no cutout
if (screenModel.hasDisplayCutout) {
CheckboxItem(
label = stringResource(R.string.pref_cutout_short),
pref = screenModel.preferences.cutoutShort(),
)
}
CheckboxItem(
label = stringResource(R.string.pref_keep_screen_on),

View File

@ -3,6 +3,7 @@ package eu.kanade.presentation.util
import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.HttpException
import eu.kanade.tachiyomi.source.online.LicensedMangaChaptersException
import tachiyomi.data.source.NoResultsException
import tachiyomi.domain.source.model.SourceNotInstalledException
@ -13,6 +14,7 @@ val Throwable.formattedMessage: String
is NoResultsException -> return getString(R.string.no_results_found)
is SourceNotInstalledException -> return getString(R.string.loader_not_implemented_error)
is HttpException -> return "$message: ${getString(R.string.http_error_hint)}"
is LicensedMangaChaptersException -> return getString(R.string.licensed_manga_chapters_error)
}
return when (val className = this::class.simpleName) {
"Exception", "IOException" -> message ?: className

View File

@ -106,10 +106,10 @@ class BackupManager(
UniFile.fromUri(context, uri)
}
)
?: throw Exception("Couldn't create backup file")
?: throw Exception(context.getString(R.string.create_backup_file_error))
if (!file.isFile) {
throw IllegalStateException("Failed to get handle on file")
throw IllegalStateException("Failed to get handle on a backup file")
}
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)

View File

@ -230,8 +230,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false)
val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
val fetchWindow by lazy { setFetchInterval.getWindow(ZonedDateTime.now()) }
val fetchWindow = setFetchInterval.getWindow(ZonedDateTime.now())
coroutineScope {
mangaToUpdate.groupBy { it.manga.source }.values
@ -265,7 +264,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate !in fetchWindow.first.rangeTo(fetchWindow.second) ->
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
else -> {

View File

@ -5,7 +5,6 @@ import androidx.annotation.CallSuper
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
@ -31,9 +30,9 @@ import tachiyomi.domain.track.model.Track as DomainTrack
abstract class TrackService(val id: Long) {
val preferences: BasePreferences by injectLazy()
val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy()
private val insertTrack: InsertTrack by injectLazy()
open val client: OkHttpClient
get() = networkService.client
@ -112,7 +111,7 @@ abstract class TrackService(val id: Long) {
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
Injekt.get<InsertTrack>().await(track)
insertTrack.await(track)
// Update chapter progress if newer chapters marked read locally
if (hasReadChapters) {
@ -120,7 +119,7 @@ abstract class TrackService(val id: Long) {
.sortedBy { it.chapterNumber }
.takeWhile { it.read }
.lastOrNull()
?.chapterNumber?.toDouble() ?: -1.0
?.chapterNumber ?: -1.0
if (latestLocalReadChapterNumber > track.lastChapterRead) {
track = track.copy(
@ -169,6 +168,7 @@ abstract class TrackService(val id: Long) {
track.last_chapter_read = chapterNumber.toFloat()
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
track.status = getCompletionStatus()
track.finished_reading_date = System.currentTimeMillis()
}
withIOContext { updateRemote(track) }
}
@ -193,7 +193,7 @@ abstract class TrackService(val id: Long) {
try {
update(track)
track.toDomainTrack(idRequired = false)?.let {
Injekt.get<InsertTrack>().await(it)
insertTrack.await(it)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" }

View File

@ -27,7 +27,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.minutes
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
@ -35,7 +35,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val authClient = client.newBuilder()
.addInterceptor(interceptor)
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
.rateLimit(permits = 85, period = 1.minutes)
.build()
suspend fun addLibManga(track: Track): Track {

View File

@ -66,7 +66,10 @@ class ExtensionManager(
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
.loadIcon(context.packageManager)
}
}
return null
}
@ -333,6 +336,7 @@ class ExtensionManager(
}
override fun onPackageUninstalled(pkgName: String) {
ExtensionLoader.uninstallPrivateExtension(context, pkgName)
unregisterExtension(pkgName)
updatePendingUpdatesCount()
}

View File

@ -32,6 +32,7 @@ sealed class Extension {
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
val isShared: Boolean,
) : Extension()
data class Available(

View File

@ -4,6 +4,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import kotlinx.coroutines.CoroutineStart
@ -27,7 +30,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
* Registers this broadcast receiver
*/
fun register(context: Context) {
context.registerReceiver(this, filter)
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
/**
@ -38,6 +41,9 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(ACTION_EXTENSION_ADDED)
addAction(ACTION_EXTENSION_REPLACED)
addAction(ACTION_EXTENSION_REMOVED)
addDataScheme("package")
}
@ -49,7 +55,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
if (isReplacing(intent)) return
launchNow {
@ -60,7 +66,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
@ -70,7 +76,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
Intent.ACTION_PACKAGE_REMOVED, ACTION_EXTENSION_REMOVED -> {
if (isReplacing(intent)) return
val pkgName = getPackageNameFromIntent(intent)
@ -121,4 +127,30 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
fun onExtensionUntrusted(extension: Extension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
companion object {
private const val ACTION_EXTENSION_ADDED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_ADDED"
private const val ACTION_EXTENSION_REPLACED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REPLACED"
private const val ACTION_EXTENSION_REMOVED = "${BuildConfig.APPLICATION_ID}.ACTION_EXTENSION_REMOVED"
fun notifyAdded(context: Context, pkgName: String) {
notify(context, pkgName, ACTION_EXTENSION_ADDED)
}
fun notifyReplaced(context: Context, pkgName: String) {
notify(context, pkgName, ACTION_EXTENSION_REPLACED)
}
fun notifyRemoved(context: Context, pkgName: String) {
notify(context, pkgName, ACTION_EXTENSION_REMOVED)
}
private fun notify(context: Context, pkgName: String, action: String) {
Intent(action).apply {
data = Uri.parse("package:$pkgName")
`package` = context.packageName
context.sendBroadcast(this)
}
}
}
}

View File

@ -11,10 +11,12 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.installer.Installer
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -156,6 +158,35 @@ internal class ExtensionInstaller(private val context: Context) {
context.startActivity(intent)
}
BasePreferences.ExtensionInstaller.PRIVATE -> {
val extensionManager = Injekt.get<ExtensionManager>()
val tempFile = File(context.cacheDir, "temp_$downloadId")
if (tempFile.exists() && !tempFile.delete()) {
// Unlikely but just in case
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
return
}
try {
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
if (ExtensionLoader.installPrivateExtensionFile(context, tempFile)) {
extensionManager.updateInstallStep(downloadId, InstallStep.Installed)
} else {
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to read downloaded extension file." }
extensionManager.updateInstallStep(downloadId, InstallStep.Error)
}
tempFile.delete()
}
else -> {
val intent = ExtensionInstallService.getIntent(context, downloadId, uri, installer)
ContextCompat.startForegroundService(context, intent)
@ -178,10 +209,15 @@ internal class ExtensionInstaller(private val context: Context) {
* @param pkgName The package name of the extension to uninstall
*/
fun uninstallApk(pkgName: String) {
if (context.isPackageInstalled(pkgName)) {
@Suppress("DEPRECATION")
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
} else {
ExtensionLoader.uninstallPrivateExtension(context, pkgName)
ExtensionInstallReceiver.notifyRemoved(context, pkgName)
}
}
/**

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.extension.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
@ -14,17 +14,28 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.File
/**
* Class that handles the loading of the extensions installed in the system.
* Class that handles the loading of the extensions. Supports two kinds of extensions:
*
* 1. Shared extension: This extension is installed to the system with package
* installer, so other variants of Tachiyomi and its forks can also use this extension.
*
* 2. Private extension: This extension is put inside private data directory of the
* running app, so this extension can only be used by the running app and not shared
* with other apps.
*
* When both kinds of extensions are installed with a same package name, shared
* extension will be used unless the version codes are different. In that case the
* one with higher version code will be used.
*/
@SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
@ -38,15 +49,14 @@ internal object ExtensionLoader {
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MIN = 1.4
const val LIB_VERSION_MAX = 1.5
private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
}
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
PackageManager.GET_META_DATA or
PackageManager.GET_SIGNATURES or
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0)
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
@ -56,8 +66,57 @@ internal object ExtensionLoader {
*/
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
private const val PRIVATE_EXTENSION_EXTENSION = "ext"
private fun getPrivateExtensionDir(context: Context) = File(context.filesDir, "exts")
fun installPrivateExtensionFile(context: Context, file: File): Boolean {
val extension = context.packageManager.getPackageArchiveInfo(file.absolutePath, PACKAGE_FLAGS)
?.takeIf { isPackageAnExtension(it) } ?: return false
val currentExtension = getExtensionPackageInfoFromPkgName(context, extension.packageName)
if (currentExtension != null) {
if (PackageInfoCompat.getLongVersionCode(extension) <
PackageInfoCompat.getLongVersionCode(currentExtension)
) {
logcat(LogPriority.ERROR) { "Installed extension version is higher. Downgrading is not allowed." }
return false
}
val extensionSignatures = getSignatures(extension)
if (extensionSignatures.isNullOrEmpty()) {
logcat(LogPriority.ERROR) { "Extension to be installed is not signed." }
return false
}
if (!extensionSignatures.containsAll(getSignatures(currentExtension)!!)) {
logcat(LogPriority.ERROR) { "Installed extension signature is not matched." }
return false
}
}
val target = File(getPrivateExtensionDir(context), "${extension.packageName}.$PRIVATE_EXTENSION_EXTENSION")
return try {
file.copyTo(target, overwrite = true)
if (currentExtension != null) {
ExtensionInstallReceiver.notifyReplaced(context, extension.packageName)
} else {
ExtensionInstallReceiver.notifyAdded(context, extension.packageName)
}
true
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to copy extension file." }
target.delete()
false
}
}
fun uninstallPrivateExtension(context: Context, pkgName: String) {
File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION").delete()
}
/**
* Return a list of all the installed extensions initialized concurrently.
* Return a list of all the available extensions initialized concurrently.
*
* @param context The application context.
*/
@ -70,16 +129,43 @@ internal object ExtensionLoader {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
val sharedExtPkgs = installedPkgs
.asSequence()
.filter { isPackageAnExtension(it) }
.map { ExtensionInfo(packageInfo = it, isShared = true) }
val privateExtPkgs = getPrivateExtensionDir(context)
.listFiles()
?.asSequence()
?.filter { it.isFile && it.extension == PRIVATE_EXTENSION_EXTENSION }
?.mapNotNull {
val path = it.absolutePath
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
?.apply { applicationInfo.fixBasePaths(path) }
}
?.filter { isPackageAnExtension(it) }
?.map { ExtensionInfo(packageInfo = it, isShared = false) }
?: emptySequence()
val extPkgs = (sharedExtPkgs + privateExtPkgs)
// Remove duplicates. Shared takes priority than private by default
.distinctBy { it.packageInfo.packageName }
// Compare version number
.mapNotNull { sharedPkg ->
val privatePkg = privateExtPkgs
.singleOrNull { it.packageInfo.packageName == sharedPkg.packageInfo.packageName }
selectExtensionPackage(sharedPkg, privatePkg)
}
.toList()
if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion
return runBlocking {
val deferred = extPkgs.map {
async { loadExtension(context, it.packageName, it) }
async { loadExtension(context, it) }
}
deferred.map { it.await() }
deferred.awaitAll()
}
}
@ -88,37 +174,61 @@ internal object ExtensionLoader {
* contains the required feature flag before trying to load it.
*/
fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
val pkgInfo = try {
val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
if (extensionPackage == null) {
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
return LoadResult.Error
}
return loadExtension(context, extensionPackage)
}
fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? {
return getExtensionInfoFromPkgName(context, pkgName)?.packageInfo
}
private fun getExtensionInfoFromPkgName(context: Context, pkgName: String): ExtensionInfo? {
val privateExtensionFile = File(getPrivateExtensionDir(context), "$pkgName.$PRIVATE_EXTENSION_EXTENSION")
val privatePkg = if (privateExtensionFile.isFile) {
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
?.takeIf { isPackageAnExtension(it) }
?.let {
it.applicationInfo.fixBasePaths(privateExtensionFile.absolutePath)
ExtensionInfo(
packageInfo = it,
isShared = false,
)
}
} else {
null
}
val sharedPkg = try {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
.takeIf { isPackageAnExtension(it) }
?.let {
ExtensionInfo(
packageInfo = it,
isShared = true,
)
}
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
logcat(LogPriority.ERROR, error)
return LoadResult.Error
null
}
if (!isPackageAnExtension(pkgInfo)) {
logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
return LoadResult.Error
}
return loadExtension(context, pkgName, pkgInfo)
return selectExtensionPackage(sharedPkg, privatePkg)
}
/**
* Loads an extension given its package name.
* Loads an extension
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
* @param extensionInfo The extension to load.
*/
private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val pkgManager = context.packageManager
val appInfo = try {
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
logcat(LogPriority.ERROR, error)
return LoadResult.Error
}
val pkgInfo = extensionInfo.packageInfo
val appInfo = pkgInfo.applicationInfo
val pkgName = pkgInfo.packageName
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
val versionName = pkgInfo.versionName
@ -139,12 +249,19 @@ internal object ExtensionLoader {
return LoadResult.Error
}
val signatureHash = getSignatureHash(context, pkgInfo)
if (signatureHash == null) {
val signatures = getSignatures(pkgInfo)
if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
} else if (!hasTrustedSignature(signatures)) {
val extension = Extension.Untrusted(
extName,
pkgName,
versionName,
versionCode,
libVersion,
signatures.last(),
)
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
return LoadResult.Untrusted(extension)
}
@ -204,12 +321,35 @@ internal object ExtensionLoader {
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature,
icon = context.getApplicationIcon(pkgName),
isUnofficial = !isOfficiallySigned(signatures),
icon = appInfo.loadIcon(pkgManager),
isShared = extensionInfo.isShared,
)
return LoadResult.Success(extension)
}
/**
* Choose which extension package to use based on version code
*
* @param shared extension installed to system
* @param private extension installed to data directory
*/
private fun selectExtensionPackage(shared: ExtensionInfo?, private: ExtensionInfo?): ExtensionInfo? {
when {
private == null && shared != null -> return shared
shared == null && private != null -> return private
shared == null && private == null -> return null
}
return if (PackageInfoCompat.getLongVersionCode(shared!!.packageInfo) >=
PackageInfoCompat.getLongVersionCode(private!!.packageInfo)
) {
shared
} else {
private
}
}
/**
* Returns true if the given package is an extension.
*
@ -220,12 +360,50 @@ internal object ExtensionLoader {
}
/**
* Returns the signature hash of the package or null if it's not signed.
* Returns the signatures of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
* @return List SHA256 digest of the signatures
*/
private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? {
val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName)
return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) }
private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val signingInfo = pkgInfo.signingInfo
if (signingInfo.hasMultipleSigners()) {
signingInfo.apkContentsSigners
} else {
signingInfo.signingCertificateHistory
}
} else {
@Suppress("DEPRECATION")
pkgInfo.signatures
}
?.map { Hash.sha256(it.toByteArray()) }
?.toList()
}
private fun hasTrustedSignature(signatures: List<String>): Boolean {
return trustedSignatures.any { signatures.contains(it) }
}
private fun isOfficiallySigned(signatures: List<String>): Boolean {
return signatures.all { it == officialSignature }
}
/**
* On Android 13+ the ApplicationInfo generated by getPackageArchiveInfo doesn't
* have sourceDir which breaks assets loading (used for getting icon here).
*/
private fun ApplicationInfo.fixBasePaths(apkPath: String) {
if (sourceDir == null) {
sourceDir = apkPath
}
if (publicSourceDir == null) {
publicSourceDir = apkPath
}
}
private data class ExtensionInfo(
val packageInfo: PackageInfo,
val isShared: Boolean,
)
}

View File

@ -4,28 +4,36 @@ import eu.kanade.domain.manga.model.hasCustomCover
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.interactor.GetTracks
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
data class MigrationFlag(
val flag: Int,
val isDefaultSelected: Boolean,
val titleId: Int,
) {
companion object {
fun create(flag: Int, defaultSelectionMap: Int, titleId: Int): MigrationFlag {
return MigrationFlag(
flag = flag,
isDefaultSelected = defaultSelectionMap and flag != 0,
titleId = titleId,
)
}
}
}
object MigrationFlags {
private const val CHAPTERS = 0b00001
private const val CATEGORIES = 0b00010
private const val TRACK = 0b00100
private const val CUSTOM_COVER = 0b01000
private const val DELETE_DOWNLOADED = 0b10000
private val coverCache: CoverCache by injectLazy()
private val getTracks: GetTracks = Injekt.get()
private val downloadCache: DownloadCache by injectLazy()
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
private var enableFlags = emptyList<Int>().toMutableList()
fun hasChapters(value: Int): Boolean {
return value and CHAPTERS != 0
}
@ -34,10 +42,6 @@ object MigrationFlags {
return value and CATEGORIES != 0
}
fun hasTracks(value: Int): Boolean {
return value and TRACK != 0
}
fun hasCustomCover(value: Int): Boolean {
return value and CUSTOM_COVER != 0
}
@ -46,34 +50,32 @@ object MigrationFlags {
return value and DELETE_DOWNLOADED != 0
}
fun getEnabledFlagsPositions(value: Int): List<Int> {
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
}
/** Returns information about applicable flags with default selections. */
fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MigrationFlag> {
val flags = mutableListOf<MigrationFlag>()
flags += MigrationFlag.create(CHAPTERS, defaultSelectedBitMap, R.string.chapters)
flags += MigrationFlag.create(CATEGORIES, defaultSelectedBitMap, R.string.categories)
fun getFlagsFromPositions(positions: Array<Int>): Int {
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
enableFlags.clear()
return fold
}
fun titles(manga: Manga?): Array<Int> {
enableFlags.add(CHAPTERS)
enableFlags.add(CATEGORIES)
val titles = arrayOf(R.string.chapters, R.string.categories).toMutableList()
if (manga != null) {
if (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) {
titles.add(R.string.track)
enableFlags.add(TRACK)
}
if (manga.hasCustomCover(coverCache)) {
titles.add(R.string.custom_cover)
enableFlags.add(CUSTOM_COVER)
flags += MigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
}
if (downloadCache.getDownloadCount(manga) > 0) {
titles.add(R.string.delete_downloaded)
enableFlags.add(DELETE_DOWNLOADED)
flags += MigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.delete_downloaded)
}
}
return titles.toTypedArray()
return flags
}
/** Returns a bit map of selected flags. */
fun getSelectedFlagsBitMap(
selectedFlags: List<Boolean>,
flags: List<MigrationFlag>,
): Int {
return selectedFlags
.zip(flags)
.filter { (isSelected, _) -> isSelected }
.map { (_, flag) -> flag.flag }
.reduceOrNull { acc, mask -> acc or mask } ?: 0
}
}

View File

@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
@ -74,15 +73,8 @@ internal fun MigrateDialog(
val scope = rememberCoroutineScope()
val state by screenModel.state.collectAsState()
val activeFlags = remember { MigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
val items = remember {
MigrationFlags.titles(oldManga)
.map { context.getString(it) }
.toList()
}
val selected = remember {
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
}
val flags = remember { MigrationFlags.getFlags(oldManga, screenModel.migrateFlags.get()) }
val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
if (state.isMigrating) {
LoadingScreen(
@ -99,18 +91,16 @@ internal fun MigrateDialog(
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
items.forEachIndexed { index, title ->
val onChange: () -> Unit = {
selected[index] = !selected[index]
}
flags.forEachIndexed { index, flag ->
val onChange = { selectedFlags[index] = !selectedFlags[index] }
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onChange),
verticalAlignment = Alignment.CenterVertically,
) {
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
Text(text = title)
Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
Text(text = context.getString(flag.titleId))
}
}
}
@ -133,7 +123,12 @@ internal fun MigrateDialog(
TextButton(
onClick = {
scope.launchIO {
screenModel.migrateManga(oldManga, newManga, false)
screenModel.migrateManga(
oldManga,
newManga,
false,
MigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
)
withUIContext { onPopScreen() }
}
},
@ -143,12 +138,13 @@ internal fun MigrateDialog(
TextButton(
onClick = {
scope.launchIO {
val selectedIndices = mutableListOf<Int>()
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
val newValue =
MigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
screenModel.migrateFlags.set(newValue)
screenModel.migrateManga(oldManga, newManga, true)
screenModel.migrateManga(
oldManga,
newManga,
true,
MigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
)
withUIContext { onPopScreen() }
}
},
@ -184,7 +180,13 @@ internal class MigrateDialogScreenModel(
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedTrackService>()
}
suspend fun migrateManga(oldManga: Manga, newManga: Manga, replace: Boolean) {
suspend fun migrateManga(
oldManga: Manga,
newManga: Manga,
replace: Boolean,
flags: Int,
) {
migrateFlags.set(flags)
val source = sourceManager.get(newManga.source) ?: return
val prevSource = sourceManager.get(oldManga.source)
@ -200,6 +202,7 @@ internal class MigrateDialogScreenModel(
newManga = newManga,
sourceChapters = chapters,
replace = replace,
flags = flags,
)
} catch (_: Throwable) {
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
@ -215,12 +218,10 @@ internal class MigrateDialogScreenModel(
newManga: Manga,
sourceChapters: List<SChapter>,
replace: Boolean,
flags: Int,
) {
val flags = migrateFlags.get()
val migrateChapters = MigrationFlags.hasChapters(flags)
val migrateCategories = MigrationFlags.hasCategories(flags)
val migrateTracks = MigrationFlags.hasTracks(flags)
val migrateCustomCover = MigrationFlags.hasCustomCover(flags)
val deleteDownloaded = MigrationFlags.hasDeleteDownloaded(flags)
@ -271,8 +272,7 @@ internal class MigrateDialogScreenModel(
}
// Update track
if (migrateTracks) {
val tracks = getTracks.await(oldManga.id).mapNotNull { track ->
getTracks.await(oldManga.id).mapNotNull { track ->
val updatedTrack = track.copy(mangaId = newManga.id)
val service = enhancedServices
@ -284,8 +284,8 @@ internal class MigrateDialogScreenModel(
updatedTrack
}
}
insertTrack.awaitAll(tracks)
}
.takeIf { it.isNotEmpty() }
?.let { insertTrack.awaitAll(it) }
// Delete downloaded
if (deleteDownloaded) {

View File

@ -25,6 +25,7 @@ import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
@ -49,9 +50,12 @@ import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.transition.platform.MaterialContainerTransform
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.manga.model.orientationType
import eu.kanade.presentation.reader.BottomReaderBar
import eu.kanade.presentation.reader.ChapterNavigator
import eu.kanade.presentation.reader.OrientationModeSelectDialog
import eu.kanade.presentation.reader.PageIndicatorText
import eu.kanade.presentation.reader.ReaderPageActionsDialog
import eu.kanade.presentation.reader.ReadingModeSelectDialog
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -78,10 +82,7 @@ import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.copy
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setComposeContent
import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
@ -94,12 +95,12 @@ import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.Constants
import tachiyomi.core.preference.toggle
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.abs
@ -124,7 +125,7 @@ class ReaderActivity : BaseActivity() {
val viewModel by viewModels<ReaderViewModel>()
private var assistUrl: String? = null
val hasCutout by lazy { hasDisplayCutout() }
private val hasCutout by lazy { hasDisplayCutout() }
/**
* Configuration at reader level, like background color or forced orientation.
@ -381,18 +382,22 @@ class ReaderActivity : BaseActivity() {
binding.pageNumber.setComposeContent {
val state by viewModel.state.collectAsState()
val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState()
if (!state.menuVisible && showPageNumber) {
PageIndicatorText(
currentPage = state.currentPage,
totalPages = state.totalPages,
)
}
}
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
val settingsScreenModel = remember {
ReaderSettingsScreenModel(
readerState = viewModel.state,
hasDisplayCutout = hasCutout,
onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType,
)
@ -423,6 +428,28 @@ class ReaderActivity : BaseActivity() {
screenModel = settingsScreenModel,
)
}
is ReaderViewModel.Dialog.ReadingModeSelect -> {
ReadingModeSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
menuToggleToast = toast(stringRes)
}
},
)
}
is ReaderViewModel.Dialog.OrientationModeSelect -> {
OrientationModeSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
menuToggleToast = toast(stringRes)
},
)
}
is ReaderViewModel.Dialog.PageActions -> {
ReaderPageActionsDialog(
onDismissRequest = onDismissRequest,
@ -435,13 +462,20 @@ class ReaderActivity : BaseActivity() {
}
}
// Init listeners on bottom menu
binding.readerNav.setComposeContent {
binding.readerMenuBottom.setComposeContent {
val state by viewModel.state.collectAsState()
if (state.viewer == null) return@setComposeContent
val isRtl = state.viewer is R2LPagerViewer
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(
isRtl = isRtl,
onNextChapter = ::loadNextChapter,
@ -455,16 +489,34 @@ class ReaderActivity : BaseActivity() {
moveToPageIndex(it)
},
)
}
initBottomShortcuts()
BottomReaderBar(
readingMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false)),
onClickReadingMode = viewModel::openReadingModeSelectDialog,
orientationMode = OrientationType.fromPreference(viewModel.getMangaOrientationType(resolveDefault = false)),
onClickOrientationMode = viewModel::openOrientationModeSelectDialog,
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(
if (enabled) {
R.string.on
} else {
R.string.off
},
)
},
onClickSettings = viewModel::openSettingsDialog,
)
}
}
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
}
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
val toolbarColor = ColorUtils.setAlphaComponent(
toolbarBackground.resolvedTintColor,
toolbarBackground.alpha,
@ -478,112 +530,6 @@ class ReaderActivity : BaseActivity() {
setMenuVisibility(viewModel.state.value.menuVisible)
}
private fun initBottomShortcuts() {
// Reading mode
with(binding.actionReadingMode) {
setTooltip(R.string.viewer)
setOnClickListener {
popupMenu(
items = ReadingModeType.entries.map { it.flagValue to it.stringRes },
selectedItemId = viewModel.getMangaReadingMode(resolveDefault = false),
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
viewModel.setMangaReadingMode(newReadingMode)
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
updateCropBordersShortcut()
}
}
}
// Crop borders
with(binding.actionCropBorders) {
setTooltip(R.string.pref_crop_borders)
setOnClickListener {
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val enabled = if (isPagerType) {
readerPreferences.cropBorders().toggle()
} else {
readerPreferences.cropBordersWebtoon().toggle()
}
menuToggleToast?.cancel()
menuToggleToast = toast(
if (enabled) {
R.string.on
} else {
R.string.off
},
)
}
}
updateCropBordersShortcut()
listOf(readerPreferences.cropBorders(), readerPreferences.cropBordersWebtoon())
.forEach { pref ->
pref.changes()
.onEach { updateCropBordersShortcut() }
.launchIn(lifecycleScope)
}
// Rotation
with(binding.actionRotation) {
setTooltip(R.string.rotation_type)
setOnClickListener {
popupMenu(
items = OrientationType.entries.map { it.flagValue to it.stringRes },
selectedItemId = viewModel.manga?.orientationType?.toInt()
?: readerPreferences.defaultOrientationType().get(),
) {
val newOrientation = OrientationType.fromPreference(itemId)
viewModel.setMangaOrientationType(newOrientation)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
}
}
}
// Settings sheet
with(binding.actionSettings) {
setTooltip(R.string.action_settings)
setOnClickListener {
viewModel.openSettingsDialog()
}
}
}
private fun updateOrientationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference)
binding.actionRotation.setImageResource(orientation.iconRes)
}
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val enabled = if (isPagerType) {
readerPreferences.cropBorders().get()
} else {
readerPreferences.cropBordersWebtoon().get()
}
binding.actionCropBorders.setImageResource(
if (enabled) {
R.drawable.ic_crop_24dp
} else {
R.drawable.ic_crop_off_24dp
},
)
}
/**
* Sets the visibility of the menu according to [visible] and with an optional parameter to
* [animate] the views.
@ -611,10 +557,6 @@ class ReaderActivity : BaseActivity() {
bottomAnimation.applySystemAnimatorScale(this)
binding.readerMenuBottom.startAnimation(bottomAnimation)
}
if (readerPreferences.showPageNumber().get()) {
config?.setPageNumberVisibility(false)
}
} else {
if (readerPreferences.fullscreen().get()) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
@ -637,10 +579,6 @@ class ReaderActivity : BaseActivity() {
bottomAnimation.applySystemAnimatorScale(this)
binding.readerMenuBottom.startAnimation(bottomAnimation)
}
if (readerPreferences.showPageNumber().get()) {
config?.setPageNumberVisibility(true)
}
}
}
@ -650,13 +588,8 @@ class ReaderActivity : BaseActivity() {
*/
private fun setManga(manga: Manga) {
val prevViewer = viewModel.state.value.viewer
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
updateCropBordersShortcut()
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
// Wait until transition is complete to avoid crash on API 26
window.sharedElementEnterTransition.doOnEnd {
@ -698,9 +631,8 @@ class ReaderActivity : BaseActivity() {
private fun showReadingModeToast(mode: Int) {
try {
val strings = resources.getStringArray(R.array.viewers_selector)
readingModeToast?.cancel()
readingModeToast = toast(strings[mode])
readingModeToast = toast(ReadingModeType.fromPreference(mode).stringRes)
} catch (e: ArrayIndexOutOfBoundsException) {
logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" }
}
@ -891,7 +823,6 @@ class ReaderActivity : BaseActivity() {
if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag
}
updateOrientationShortcut(viewModel.getMangaOrientationType(resolveDefault = false))
}
/**
@ -955,10 +886,6 @@ class ReaderActivity : BaseActivity() {
}
.launchIn(lifecycleScope)
readerPreferences.showPageNumber().changes()
.onEach(::setPageNumberVisibility)
.launchIn(lifecycleScope)
readerPreferences.trueColor().changes()
.onEach(::setTrueColor)
.launchIn(lifecycleScope)
@ -1008,13 +935,6 @@ class ReaderActivity : BaseActivity() {
}
}
/**
* Sets the visibility of the bottom page indicator according to [visible].
*/
fun setPageNumberVisibility(visible: Boolean) {
binding.pageNumber.isVisible = visible
}
/**
* Sets the 32-bit color mode according to [enabled].
*/

View File

@ -61,7 +61,7 @@ class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet)
override fun onDraw(canvas: Canvas) {
if (navigation == null) return
navigation?.regions?.forEach { region ->
navigation?.getRegions()?.forEach { region ->
val rect = region.rectF
// Scale rect from 1f,1f to screen width and height

View File

@ -54,6 +54,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.preference.toggle
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withIOContext
@ -78,8 +79,8 @@ import java.util.Date
/**
* Presenter used by the activity to perform background operations.
*/
class ReaderViewModel(
private val savedState: SavedStateHandle = SavedStateHandle(),
class ReaderViewModel @JvmOverloads constructor(
private val savedState: SavedStateHandle,
private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val downloadProvider: DownloadProvider = Injekt.get(),
@ -119,6 +120,15 @@ class ReaderViewModel(
field = value
}
/**
* The visible page index of the currently loaded chapter. Used to restore from process kill.
*/
private var chapterPageIndex = savedState.get<Int>("page_index") ?: -1
set(value) {
savedState["page_index"] = value
field = value
}
/**
* The chapter loader for the loaded manga. It'll be null until [manga] is set.
*/
@ -197,7 +207,10 @@ class ReaderViewModel(
.distinctUntilChanged()
.filterNotNull()
.onEach { currentChapter ->
if (!currentChapter.chapter.read) {
if (chapterPageIndex >= 0) {
// Restore from SavedState
currentChapter.requestedPage = chapterPageIndex
} else if (!currentChapter.chapter.read) {
currentChapter.requestedPage = currentChapter.chapter.last_page_read
}
chapterId = currentChapter.chapter.id!!
@ -489,6 +502,7 @@ class ReaderViewModel(
it.copy(currentPage = pageIndex + 1)
}
readerChapter.requestedPage = pageIndex
chapterPageIndex = pageIndex
if (!incognitoMode && page.status != Page.State.ERROR) {
readerChapter.chapter.last_page_read = pageIndex
@ -661,6 +675,15 @@ class ReaderViewModel(
}
}
fun toggleCropBorders(): Boolean {
val isPagerType = ReadingModeType.isPagerType(getMangaReadingMode())
return if (isPagerType) {
readerPreferences.cropBorders().toggle()
} else {
readerPreferences.cropBordersWebtoon().toggle()
}
}
/**
* Generate a filename for the given [manga] and [page]
*/
@ -683,6 +706,14 @@ class ReaderViewModel(
mutableState.update { it.copy(dialog = Dialog.Loading) }
}
fun openReadingModeSelectDialog() {
mutableState.update { it.copy(dialog = Dialog.ReadingModeSelect) }
}
fun openOrientationModeSelectDialog() {
mutableState.update { it.copy(dialog = Dialog.OrientationModeSelect) }
}
fun openPageDialog(page: ReaderPage) {
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
}
@ -863,6 +894,8 @@ class ReaderViewModel(
sealed interface Dialog {
data object Loading : Dialog
data object Settings : Dialog
data object ReadingModeSelect : Dialog
data object OrientationModeSelect : Dialog
data class PageActions(val page: ReaderPage) : Dialog
}

View File

@ -5,14 +5,14 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
DEFAULT(0, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000),
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008),
PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010),
LANDSCAPE(3, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018),
LOCKED_PORTRAIT(4, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020),
LOCKED_LANDSCAPE(5, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028),
REVERSE_PORTRAIT(6, ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030),
enum class OrientationType(val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
DEFAULT(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.label_default, R.drawable.ic_screen_rotation_24dp, 0x00000000),
FREE(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp, 0x00000008),
PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000010),
LANDSCAPE(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_landscape, R.drawable.ic_stay_current_landscape_24dp, 0x00000018),
LOCKED_PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp, 0x00000020),
LOCKED_LANDSCAPE(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp, 0x00000028),
REVERSE_PORTRAIT(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT, R.string.rotation_reverse_portrait, R.drawable.ic_stay_current_portrait_24dp, 0x00000030),
;
companion object {

View File

@ -13,6 +13,7 @@ import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel(
readerState: StateFlow<ReaderViewModel.State>,
val hasDisplayCutout: Boolean,
val onChangeReadingMode: (ReadingModeType) -> Unit,
val onChangeOrientation: (OrientationType) -> Unit,
val preferences: ReaderPreferences = Injekt.get(),

View File

@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
enum class ReadingModeType(val prefValue: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
DEFAULT(0, R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000),
LEFT_TO_RIGHT(1, R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001),
RIGHT_TO_LEFT(2, R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002),
VERTICAL(3, R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003),
WEBTOON(4, R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004),
CONTINUOUS_VERTICAL(5, R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005),
enum class ReadingModeType(@StringRes val stringRes: Int, @DrawableRes val iconRes: Int, val flagValue: Int) {
DEFAULT(R.string.label_default, R.drawable.ic_reader_default_24dp, 0x00000000),
LEFT_TO_RIGHT(R.string.left_to_right_viewer, R.drawable.ic_reader_ltr_24dp, 0x00000001),
RIGHT_TO_LEFT(R.string.right_to_left_viewer, R.drawable.ic_reader_rtl_24dp, 0x00000002),
VERTICAL(R.string.vertical_viewer, R.drawable.ic_reader_vertical_24dp, 0x00000003),
WEBTOON(R.string.webtoon_viewer, R.drawable.ic_reader_webtoon_24dp, 0x00000004),
CONTINUOUS_VERTICAL(R.string.vertical_plus_viewer, R.drawable.ic_reader_continuous_vertical_24dp, 0x00000005),
;
companion object {

View File

@ -32,15 +32,19 @@ abstract class ViewerNavigation {
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
abstract var regions: List<Region>
var invertMode: ReaderPreferences.TappingInvertMode = ReaderPreferences.TappingInvertMode.NONE
protected abstract var regionList: List<Region>
/** Returns regions with applied inversion. */
fun getRegions(): List<Region> {
return regionList.map { it.invert(invertMode) }
}
fun getAction(pos: PointF): NavigationRegion {
val x = pos.x
val y = pos.y
val region = regions.map { it.invert(invertMode) }
.find { it.rectF.contains(x, y) }
val region = getRegions().find { it.rectF.contains(x, y) }
return when {
region != null -> region.type
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU

View File

@ -14,5 +14,5 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/
class DisabledNavigation : ViewerNavigation() {
override var regions: List<Region> = emptyList()
override var regionList: List<Region> = emptyList()
}

View File

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/
class EdgeNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
override var regionList: List<Region> = listOf(
Region(
rectF = RectF(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT,

View File

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/
class KindlishNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
override var regionList: List<Region> = listOf(
Region(
rectF = RectF(0.33f, 0.33f, 1f, 1f),
type = NavigationRegion.NEXT,

View File

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/
open class LNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
override var regionList: List<Region> = listOf(
Region(
rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
type = NavigationRegion.PREV,

View File

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/
class RightAndLeftNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf(
override var regionList: List<Region> = listOf(
Region(
rectF = RectF(0f, 0f, 0.33f, 1f),
type = NavigationRegion.LEFT,

View File

@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class AnilistLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(data?.fragment.toString())
if (matchResult?.groups?.get(1) != null) {
lifecycleScope.launchIO {
trackManager.aniList.login(matchResult.groups[1]!!.value)
returnToSettings()
}
} else {
trackManager.aniList.logout()
returnToSettings()
}
}
}

View File

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class BangumiLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.bangumi.login(code)
returnToSettings()
}
} else {
trackManager.bangumi.logout()
returnToSettings()
}
}
}

View File

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class MyAnimeListLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.myAnimeList.login(code)
returnToSettings()
}
} else {
trackManager.myAnimeList.logout()
returnToSettings()
}
}
}

View File

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.shikimori.login(code)
returnToSettings()
}
} else {
trackManager.shikimori.logout()
returnToSettings()
}
}
}

View File

@ -0,0 +1,70 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class TrackLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
when (data?.host) {
"anilist-auth" -> handleAnilist(data)
"bangumi-auth" -> handleBangumi(data)
"myanimelist-auth" -> handleMyAnimeList(data)
"shikimori-auth" -> handleShikimori(data)
}
}
private fun handleAnilist(data: Uri) {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(data.fragment.toString())
if (matchResult?.groups?.get(1) != null) {
lifecycleScope.launchIO {
trackManager.aniList.login(matchResult.groups[1]!!.value)
returnToSettings()
}
} else {
trackManager.aniList.logout()
returnToSettings()
}
}
private fun handleBangumi(data: Uri) {
val code = data.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.bangumi.login(code)
returnToSettings()
}
} else {
trackManager.bangumi.logout()
returnToSettings()
}
}
private fun handleMyAnimeList(data: Uri) {
val code = data.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.myAnimeList.login(code)
returnToSettings()
}
} else {
trackManager.myAnimeList.logout()
returnToSettings()
}
}
private fun handleShikimori(data: Uri) {
val code = data.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.shikimori.login(code)
returnToSettings()
}
} else {
trackManager.shikimori.logout()
returnToSettings()
}
}
}

View File

@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.util.chapter
import eu.kanade.tachiyomi.data.download.DownloadCache
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.source.local.isLocal
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Returns a copy of the list with not downloaded chapters removed
* Returns a copy of the list with not downloaded chapters removed.
*/
fun List<Chapter>.filterDownloaded(manga: Manga): List<Chapter> {
if (manga.isLocal()) return this
val downloadCache: DownloadCache = Injekt.get()
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }

View File

@ -7,20 +7,13 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.PermissionChecker
import androidx.core.content.getSystemService
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.domain.ui.UiPreferences
@ -35,7 +28,6 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import kotlin.math.roundToInt
/**
* Copies a string to clipboard
@ -69,25 +61,6 @@ fun Context.copyToClipboard(label: String, content: String) {
*/
fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED
/**
* Returns the color for the given attribute.
*
* @param resource the attribute.
* @param alphaFactor the alpha number [0,1].
*/
@ColorInt fun Context.getResourceColor(@AttrRes resource: Int, alphaFactor: Float = 1f): Int {
val typedArray = obtainStyledAttributes(intArrayOf(resource))
val color = typedArray.getColor(0, 0)
typedArray.recycle()
if (alphaFactor < 1f) {
val alpha = (color.alpha * alphaFactor).roundToInt()
return Color.argb(alpha, color.red, color.green, color.blue)
}
return color
}
val Context.powerManager: PowerManager
get() = getSystemService()!!

View File

@ -2,11 +2,8 @@
package eu.kanade.tachiyomi.util.view
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
@ -14,11 +11,7 @@ import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@ -27,11 +20,8 @@ import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.forEach
import com.google.android.material.shape.MaterialShapeDrawable
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
inline fun ComponentActivity.setComposeContent(
parent: CompositionContext? = null,
@ -65,24 +55,6 @@ fun ComposeView.setComposeContent(
}
}
/**
* Adds a tooltip shown on long press.
*
* @param stringRes String resource for tooltip.
*/
inline fun View.setTooltip(@StringRes stringRes: Int) {
setTooltip(context.getString(stringRes))
}
/**
* Adds a tooltip shown on long press.
*
* @param text Text for tooltip.
*/
inline fun View.setTooltip(text: String) {
TooltipCompat.setTooltipText(this, text)
}
/**
* Shows a popup menu on top of this view.
*
@ -110,57 +82,6 @@ inline fun View.popupMenu(
return popup
}
/**
* Shows a popup menu on top of this view.
*
* @param items menu item names to inflate the menu with. List of itemId to stringRes pairs.
* @param selectedItemId optionally show a checkmark beside an item with this itemId.
* @param onMenuItemClick function to execute when a menu item is clicked.
*/
@SuppressLint("RestrictedApi")
inline fun View.popupMenu(
items: List<Pair<Int, Int>>,
selectedItemId: Int? = null,
noinline onMenuItemClick: MenuItem.() -> Unit,
): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
items.forEach { (id, stringRes) ->
popup.menu.add(0, id, 0, stringRes)
}
if (selectedItemId != null) {
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
val emptyIcon = AppCompatResources.getDrawable(context, R.drawable.ic_blank_24dp)
popup.menu.forEach { item ->
item.icon = when (item.itemId) {
selectedItemId -> AppCompatResources.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
else -> emptyIcon
}
}
}
popup.setOnMenuItemClickListener {
it.onMenuItemClick()
true
}
popup.show()
return popup
}
/**
* Returns a deep copy of the provided [Drawable]
*/
inline fun <reified T : Drawable> T.copy(context: Context): T? {
return (constantState?.newDrawable()?.mutate() as? T).apply {
if (this is MaterialShapeDrawable) {
initializeElevationOverlay(context)
}
}
}
fun View?.isVisibleOnScreen(): Boolean {
if (this == null) {
return false

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View File

@ -1,5 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -57,83 +56,11 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" />
<LinearLayout
<androidx.compose.ui.platform.ComposeView
android:id="@+id/reader_menu_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/reader_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layoutDirection="ltr" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar_bottom"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:clickable="true"
tools:ignore="KeyboardInaccessibleWidget">
<ImageButton
android:id="@+id/action_reading_mode"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/viewer"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_crop_borders"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_reader_default_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_crop_borders"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pref_crop_borders"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_rotation"
app:layout_constraintStart_toEndOf="@+id/action_reading_mode"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_crop_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_rotation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pref_rotation_type"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_settings"
app:layout_constraintStart_toEndOf="@+id/action_crop_borders"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_screen_rotation_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_settings"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
android:layout_gravity="bottom" />
</FrameLayout>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="viewers_selector">
<item>@string/label_default</item>
<item>@string/left_to_right_viewer</item>
<item>@string/right_to_left_viewer</item>
<item>@string/vertical_viewer</item>
<item>@string/webtoon_viewer</item>
<item>@string/vertical_plus_viewer</item>
</string-array>
<string-array name="rotation_type">
<item>@string/label_default</item>
<item>@string/rotation_free</item>
<item>@string/rotation_portrait</item>
<item>@string/rotation_landscape</item>
<item>@string/rotation_force_portrait</item>
<item>@string/rotation_force_landscape</item>
<item>@string/rotation_reverse_portrait</item>
</string-array>
</resources>

View File

@ -8,10 +8,17 @@ import java.io.IOException
import java.util.ArrayDeque
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toDuration
import kotlin.time.toDurationUnit
/**
* An OkHttp interceptor that handles rate limiting.
*
* This uses `java.time` APIs and is the legacy method, kept
* for compatibility reasons with existing extensions.
*
* Examples:
*
* permits = 5, period = 1, unit = seconds => 5 requests per second
@ -19,27 +26,43 @@ import java.util.concurrent.TimeUnit
*
* @since extension-lib 1.3
*
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Long] The limiting duration. Defaults to 1.
* @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
*/
@Deprecated("Use the version with kotlin.time APIs instead.")
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(null, permits, period, unit))
) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit())))
/**
* An OkHttp interceptor that handles rate limiting.
*
* Examples:
*
* permits = 5, period = 1.seconds => 5 requests per second
* permits = 10, period = 2.minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.5
*
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) =
addInterceptor(RateLimitInterceptor(null, permits, period))
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor(
private val host: String?,
private val permits: Int,
period: Long,
unit: TimeUnit,
period: Duration,
) : Interceptor {
private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
private val rateLimitMillis = period.inWholeMilliseconds
private val fairLock = Semaphore(1, true)
override fun intercept(chain: Interceptor.Chain): Response {

View File

@ -1,12 +1,20 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toDuration
import kotlin.time.toDurationUnit
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* This uses Java Time APIs and is the legacy method, kept
* for compatibility reasons with existing extensions.
*
* Examples:
*
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
@ -14,14 +22,55 @@ import java.util.concurrent.TimeUnit
*
* @since extension-lib 1.3
*
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
* @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Long] The limiting duration. Defaults to 1.
* @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
*/
@Deprecated("Use the version with kotlin.time APIs instead.")
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period, unit))
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* httpUrl = "https://api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
* httpUrl = "https://imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.5
*
* @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period))
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* url = "https://api.manga.com", permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
* url = "https://imagecdn.manga.com", permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.5
*
* @param url [String] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
url: String,
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))

View File

@ -58,7 +58,7 @@ class ChapterRepositoryImpl(
read = chapterUpdate.read,
bookmark = chapterUpdate.bookmark,
lastPageRead = chapterUpdate.lastPageRead,
chapterNumber = chapterUpdate.chapterNumber?.toDouble(),
chapterNumber = chapterUpdate.chapterNumber,
sourceOrder = chapterUpdate.sourceOrder,
dateFetch = chapterUpdate.dateFetch,
dateUpload = chapterUpdate.dateUpload,

View File

@ -6,8 +6,10 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import tachiyomi.domain.chapter.model.Chapter
import java.time.Duration
import java.time.ZonedDateTime
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
import kotlin.time.toJavaDuration
@Execution(ExecutionMode.CONCURRENT)
class SetFetchIntervalTest {
@ -22,49 +24,34 @@ class SetFetchIntervalTest {
@Test
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
val chapters = mutableListOf<Chapter>()
(1..1).forEach {
val duration = Duration.ofHours(10)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..2).map {
chapterWithTime(chapter, 10.hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
}
@Test
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(10)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, 10.hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
}
@Test
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
val chapters = mutableListOf<Chapter>()
(1..2).forEach {
val duration = Duration.ofHours(24L)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
}
(1..5).forEach {
val duration = Duration.ofHours(48L)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..2).map {
chapterWithTime(chapter, 24.hours)
} + (1..5).map {
chapterWithTime(chapter, 48.hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
}
@Test
fun `calculateInterval returns default of 1 day when interval less than 1`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(15L * it)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, (15 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@ -72,61 +59,46 @@ class SetFetchIntervalTest {
// Normal interval calculation
@Test
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(24L * it)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, (24 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@Test
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(48L * it)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, (48 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
}
@Test
fun `calculateInterval returns floored value when interval is decimal`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(25L * it)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, (25 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@Test
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(43L * it)
val newChapter = chapterAddTime(chapter, duration)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, (43 * it).hours)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
@Test
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
val chapters = mutableListOf<Chapter>()
(1..5).forEach {
val duration = Duration.ofHours(25L * it)
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
chapters.add(newChapter)
val chapters = (1..5).map {
chapterWithTime(chapter, (25 * it).hours).copy(dateUpload = 0L)
}
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
}
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.plus(duration).toEpochSecond() * 1000
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
}
}

View File

@ -6,11 +6,11 @@ paging_version = "3.2.0"
[libraries]
gradle = { module = "com.android.tools.build:gradle", version.ref = "agp_version" }
annotation = "androidx.annotation:annotation:1.7.0-alpha03"
annotation = "androidx.annotation:annotation:1.7.0-beta01"
appcompat = "androidx.appcompat:appcompat:1.6.1"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
corektx = "androidx.core:core-ktx:1.12.0-beta01"
corektx = "androidx.core:core-ktx:1.12.0-rc01"
splashscreen = "androidx.core:core-splashscreen:1.0.1"
recyclerview = "androidx.recyclerview:recyclerview:1.3.1"
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
@ -22,12 +22,12 @@ lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.r
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
guava = "com.google.guava:guava:32.0.1-android"
guava = "com.google.guava:guava:32.1.2-android"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-beta02"
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-beta03"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"

View File

@ -1,7 +1,7 @@
[versions]
compiler = "1.5.1"
compose-bom = "2023.07.00-alpha02"
accompanist = "0.31.5-beta"
compose-bom = "2023.09.00-alpha01"
accompanist = "0.33.0-alpha"
[libraries]
activity = "androidx.activity:activity-compose:1.7.2"

View File

@ -20,7 +20,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
okio = "com.squareup.okio:okio:3.4.0"
okio = "com.squareup.okio:okio:3.5.0"
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
@ -36,7 +36,7 @@ sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref =
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
sqlite-android = "com.github.requery:sqlite-android:3.42.0"
preferencektx = "androidx.preference:preference-ktx:1.2.0"
preferencektx = "androidx.preference:preference-ktx:1.2.1"
injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
@ -65,7 +65,7 @@ swipe = "me.saket.swipe:swipe:1.2.0"
logcat = "com.squareup.logcat:logcat:0.1"
acra-http = "ch.acra:acra-http:5.11.0"
acra-http = "ch.acra:acra-http:5.11.1"
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.3.0"
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }

View File

@ -855,17 +855,7 @@
<string name="action_filter_interval_dropped">ربما أُلغيت؟ ٢٠ التماسًا متأخرًا وشهران</string>
<string name="manga_display_interval_title">قدِّر كلَّ</string>
<string name="pref_update_only_in_release_period">ليس ضمن مدة الإصدار المتوقعة</string>
<string name="pref_update_release_grace_period">مدة سماح الإصدار المتوقعة</string>
<string name="pref_update_release_grace_period_info">مدة سماح قصيرة خير من مدة سماح طويلة، وذلك لتخفف الضغط على المصادر. وكلما تُلتمس مدخلة وليس لهذا تحديث تصير مدة الالتماس أطول، وأقصاها ٢٨ يومًا.</string>
<string name="manga_modify_calculated_interval_title">خصِّص المدة</string>
<plurals name="pref_update_release_following_days">
<item quantity="zero">لا يوم بعد</item>
<item quantity="one">بعد يوم واحد</item>
<item quantity="two">بعد يومين</item>
<item quantity="few">بعد %d أيام</item>
<item quantity="many">بعد %d يومًا</item>
<item quantity="other">بعد %d يوم</item>
</plurals>
<string name="skipped_reason_not_in_release_period">تُخُطِّيت بسبب عدم توقع صدور اليوم</string>
<string name="manga_display_modified_interval_title">عيِّن التحديث كلَّ</string>
<plurals name="day">
@ -876,14 +866,6 @@
<item quantity="many">%d يومًا</item>
<item quantity="other">%d يوم</item>
</plurals>
<plurals name="pref_update_release_leading_days">
<item quantity="zero">لا يوم قبل</item>
<item quantity="one">قبل يوم واحد</item>
<item quantity="two">قبل يومين</item>
<item quantity="few">قبل %d أيام</item>
<item quantity="many">قبل %d يومًا</item>
<item quantity="other">قبل %d يوم</item>
</plurals>
<string name="intervals_header">المدة</string>
<string name="action_sort_next_updated">التحديث المتوقع القادم</string>
<string name="track_delete_title">أأزيل تتبع %s؟</string>

View File

@ -788,8 +788,6 @@
<string name="action_copy_to_clipboard">Copiado al portapapeles</string>
<string name="delete_downloaded">Eliminar descargado</string>
<string name="pref_update_only_in_release_period">Fuera del período de publicación esperado</string>
<string name="pref_update_release_grace_period">Período de gracia de publicación esperado</string>
<string name="pref_update_release_grace_period_info">Se recomienda un período de gracia corto para evitar sobrecargar las páginas de descarga. Cuántas más veces fallen las comprobaciones mayor será el espacio de tiempo hasta un tope de 28 días.</string>
<string name="pref_chapter_swipe_start">Deslizar a la izquierda</string>
<string name="manga_modify_calculated_interval_title">Personalizar intervalo</string>
<string name="pref_double_tap_zoom">Tocar dos veces para ampliar</string>
@ -800,11 +798,6 @@
<string name="pref_library_columns_per_row">%d por fila</string>
<string name="intervals_header">Intervalos</string>
<string name="skipped_reason_not_in_release_period">Omitido porque no se espera ninguna actualización hoy</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d día despues</item>
<item quantity="many">%d días despues</item>
<item quantity="other">%d días despues</item>
</plurals>
<string name="split_tall_images">Dividir imágenes largas</string>
<string name="pref_page_rotate">Girar las páginas anchas para adaptarlas a la pantalla</string>
<string name="pref_page_rotate_invert">Girar las páginas anchas en la dirección opuesta</string>
@ -821,11 +814,6 @@
<item quantity="many">Faltan %1$s capítulos</item>
<item quantity="other">Faltan %1$s capítulos</item>
</plurals>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d día antes</item>
<item quantity="many">%d días antes</item>
<item quantity="other">%d días antes</item>
</plurals>
<string name="manga_display_interval_title">Estimar cada</string>
<string name="manga_display_modified_interval_title">Establecer la actualización cada</string>
<string name="action_ok">Aceptar</string>

View File

@ -790,14 +790,6 @@
<string name="action_set_interval">Estableix linterval</string>
<string name="action_filter_interval_custom">Interval dobtenció personalitzat</string>
<string name="action_sort_next_updated">Pròxima actualització prevista</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d dia abans</item>
<item quantity="other">%d dies abans</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d dia després</item>
<item quantity="other">%d dies després</item>
</plurals>
<plurals name="day">
<item quantity="one">1 dia</item>
<item quantity="other">%d dies</item>
@ -807,7 +799,6 @@
<string name="action_filter_interval_dropped">Abandonat\? Endarrerit 20 o més dies i 2 mesos</string>
<string name="action_ok">Dacord</string>
<string name="pref_update_only_in_release_period">Fora del període esperat de publicació</string>
<string name="pref_update_release_grace_period">Període de gràcia de publicació prevista</string>
<string name="intervals_header">Intervals</string>
<string name="manga_display_interval_title">Estima cada</string>
<string name="manga_display_modified_interval_title">Sactualitzarà cada</string>
@ -817,7 +808,6 @@
<string name="track_delete_text">Se neliminarà el seguiment local.</string>
<string name="skipped_reason_not_in_release_period">Sha omès perquè no se nespera cap publicació avui</string>
<string name="action_filter_interval_passed">Període de comprovació superat</string>
<string name="pref_update_release_grace_period_info">Es recomana un període de gràcia baix per a minimitzar la sobrecàrrega de les fonts. Com més comprovacions fallides es produeixin, més interval hi haurà entre comprovacions, fins a un màxim de 28 dies.</string>
<string name="delete_downloaded">Suprimeix els baixats</string>
<string name="has_results">Té resultats</string>
<string name="syncing_library">Sestà sincronitzant la biblioteca</string>

View File

@ -812,24 +812,12 @@
<string name="manga_display_modified_interval_title">Nastavit aktualizaci každých</string>
<string name="action_set_interval">Nastavit interval</string>
<string name="action_filter_interval_dropped">Opuštěný\? Pozdě 20+ a 2 měsíce</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">Před %d dnem</item>
<item quantity="few">Před %d dny</item>
<item quantity="other">před %d dny</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">po %d dni</item>
<item quantity="few">po %d dnech</item>
<item quantity="other">po %d dnech</item>
</plurals>
<plurals name="day">
<item quantity="one">1 den</item>
<item quantity="few">%d dny</item>
<item quantity="other">%d dní</item>
</plurals>
<string name="pref_update_only_in_release_period">Mimo očekávané období vydání</string>
<string name="pref_update_release_grace_period_info">Pro minimalizaci zátěže zdrojů se doporučuje nízká doba odkladu. Čím více kontrol záznamu bude zmeškáno, tím delší bude interval mezi kontrolami, maximálně však 28 dní.</string>
<string name="pref_update_release_grace_period">Očekávané období tolerance vydání</string>
<string name="intervals_header">Intervaly</string>
<string name="skipped_reason_not_in_release_period">Přeskočeno, protože dnes nebylo očekáváno žádné vydání</string>
<string name="track_delete_text">Tím se lokálně odstraní sledování.</string>
@ -840,4 +828,7 @@
<string name="has_results">Má výsledky</string>
<string name="syncing_library">Synchronizace knihovny</string>
<string name="library_sync_complete">Synchronizace knihovny dokončena</string>
<string name="track_activity_name">Sledování přihlášení</string>
<string name="information_cloudflare_help">Klepněte zde pro pomoc s Cloudflare</string>
<string name="download_cache_invalidated">Index stažených zneplatněn</string>
</resources>

View File

@ -797,18 +797,8 @@
<string name="action_filter_interval_long">Monatlich abrufen (28 Tage)</string>
<string name="action_sort_next_updated">Nächste erwartete Aktualisierung</string>
<string name="pref_update_only_in_release_period">Außerhalb des erwarteten Veröffentlichungszeitraums</string>
<string name="pref_update_release_grace_period">Erwarteter Veröffentlichungstoleranzzeitraum</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">1 Tag davor</item>
<item quantity="other">%d Tage davor</item>
</plurals>
<string name="manga_modify_calculated_interval_title">Intervall anpassen</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">1 Tag danach</item>
<item quantity="other">%d Tage danach</item>
</plurals>
<string name="skipped_reason_not_in_release_period">Übersprungen, da heute keine Veröffentlichung erwartet wurde</string>
<string name="pref_update_release_grace_period_info">Ein niedriger Toleranzzeitraum wird empfohlen, um die Auslastung der Quellen zu minimieren. Je mehr Überprüfungen für einen Eintrag fehlschlagen, desto länger wird das Intervall zwischen den Überprüfungen, mit einem Maximum von 28 Tagen.</string>
<string name="manga_display_interval_title">Schätzt alle</string>
<string name="manga_display_modified_interval_title">Aktualisiert alle</string>
<string name="action_filter_interval_dropped">Abgebrochen\? Um 20+ Tage und 2 Monate verspätet</string>
@ -823,8 +813,6 @@
<string name="syncing_library">Bibliothek wird synchronisiert</string>
<string name="library_sync_complete">Bibliothekssynchronisierung abgeschlossen</string>
<string name="information_cloudflare_help">Tippe hier, um Hilfe zu Cloudflare zu erhalten</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d %2$d Tag</item>
<item quantity="other">%1$d - %2$d Tage</item>
</plurals>
<string name="download_cache_invalidated">Index der Downloads invalide</string>
<string name="track_activity_name">Tracking-Login</string>
</resources>

View File

@ -794,11 +794,6 @@
<string name="action_filter_interval_passed">Πέρασε την περίοδο ελέγχου</string>
<string name="pref_update_only_in_release_period">Εκτός αναμενόμενης περιόδου κυκλοφορίας</string>
<string name="intervals_header">Διαστήματα</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d ημέρα μετά</item>
<item quantity="other">%d ημέρες μετά</item>
</plurals>
<string name="pref_update_release_grace_period_info">Συνιστάται χαμηλή περίοδος χάριτος για την ελαχιστοποίηση της πίεσης στις πηγές. Όσο περισσότεροι έλεγχοι για μια καταχώρηση παραλείπονται, τόσο μεγαλύτερο θα είναι το διάστημα μεταξύ των ελέγχων με μέγιστο όριο τις 28 ημέρες.</string>
<plurals name="day">
<item quantity="one">1 ημέρα</item>
<item quantity="other">%d ημέρες</item>
@ -807,11 +802,6 @@
<string name="manga_display_modified_interval_title">Ρύθμιση για ενημέρωση κάθε</string>
<string name="action_filter_interval_long">Ανάκτηση μηνιαίως (28 ημέρες)</string>
<string name="action_sort_next_updated">Επόμενη αναμενόμενη ενημέρωση</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d ημέρα πριν</item>
<item quantity="other">%d ημέρες πριν</item>
</plurals>
<string name="pref_update_release_grace_period">Αναμενόμενη περίοδος χάριτος κυκλοφορίας</string>
<string name="manga_modify_calculated_interval_title">Προσαρμογή διαστήματος</string>
<string name="skipped_reason_not_in_release_period">Παραλείφθηκε επειδή δεν αναμενόταν κυκλοφορία σήμερα</string>
<string name="action_ok">Εντάξει</string>
@ -823,9 +813,5 @@
<string name="syncing_library">Συγχρονισμός βιβλιοθήκης</string>
<string name="library_sync_complete">Ο συγχρονισμός βιβλιοθήκης ολοκληρώθηκε</string>
<string name="information_cloudflare_help">Πατήστε εδώ για βοήθεια με το Cloudflare</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d ημέρα</item>
<item quantity="other">%1$d - %2$d ημέρες</item>
</plurals>
<string name="download_cache_invalidated">Το ευρετήριο λήψεων ακυρώθηκε</string>
</resources>

View File

@ -837,16 +837,6 @@
<string name="action_sort_next_updated">Próxima actualización prevista</string>
<string name="pref_update_only_in_release_period">Fuera del período de publicación esperado</string>
<string name="intervals_header">Intervalos</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d día antes</item>
<item quantity="many">%d días antes</item>
<item quantity="other">%d días antes</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d día después</item>
<item quantity="many">%d días después</item>
<item quantity="other">%d días después</item>
</plurals>
<string name="action_filter_interval_passed">Ha pasado el período de comprobación</string>
<string name="manga_display_interval_title">Estimar cada</string>
<plurals name="day">
@ -856,8 +846,6 @@
</plurals>
<string name="manga_modify_calculated_interval_title">Personalizar intervalo</string>
<string name="action_filter_interval_dropped">¿Abandonado\? Tras más de 20 días y 2 meses</string>
<string name="pref_update_release_grace_period">Período de gracia de publicación esperado</string>
<string name="pref_update_release_grace_period_info">Se recomienda un período de gracia corto para evitar sobrecargar las páginas de descarga. Cuántas más veces fallen las comprobaciones mayor será el espacio de tiempo hasta un tope de 28 días.</string>
<string name="manga_display_modified_interval_title">Forzar actualización cada</string>
<string name="skipped_reason_not_in_release_period">No se ha comprobado ninguna actualización hoy al no esperar ningún cambio</string>
<string name="action_set_interval">Establecer intervalo</string>
@ -872,9 +860,6 @@
<string name="library_sync_complete">La biblioteca se ha sincronizado correctamente</string>
<string name="syncing_library">Sincronizando la biblioteca</string>
<string name="information_cloudflare_help">Toca aquí para solucionar problemas de acceso con Cloudflare</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d día</item>
<item quantity="many">%1$d - %2$d días</item>
<item quantity="other">%1$d - %2$d días</item>
</plurals>
<string name="download_cache_invalidated">Se ha borrado el índice de descargas</string>
<string name="track_activity_name">Inicio de sesión de seguimiento</string>
</resources>

View File

@ -733,7 +733,7 @@
<string name="track_remove_date_conf_title">Tanggalin ang petsa\?</string>
<string name="track_remove_start_date_conf_text">Tatanggalin nito ang huling petsa na ipinili mo na simula sa %s</string>
<string name="track_remove_finish_date_conf_text">Aalisin nito ang lahat ng mga nauna mong napiling petsa ng kayarian magmula sa %s</string>
<string name="pref_invalidate_download_cache">Ipawalang-bisa ang indise ng downloads</string>
<string name="pref_invalidate_download_cache">Ipawalang-bisa ang indise ng mga download</string>
<string name="pref_invalidate_download_cache_summary">Pilitin ang app na tingnan kung may naka-download</string>
<string name="label_completed_titles">Mga natapos na entry</string>
<string name="label_started">Nasimulan</string>
@ -794,15 +794,6 @@
<string name="action_filter_interval_dropped">Nawala\? or Nahulog\? (depending on the context, \"Nahulog\" means dropped or dropped something, and \"Nawala\" means Gone/Vanished) Nahuling 20+ at 2 buwan</string>
<string name="action_filter_interval_passed">Lumipas ang check period</string>
<string name="action_filter_interval_long">Kunin kada buwan (kada ika-28 na araw)</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">pagkatapos ng %d araw</item>
<item quantity="other">pagkatapos ng %d (mga) araw</item>
</plurals>
<string name="pref_update_release_grace_period">Inaasahang paglabas sa grace period</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">bago ang %d araw</item>
<item quantity="other">bago ang %d (mga) araw</item>
</plurals>
<plurals name="day">
<item quantity="one">1 araw</item>
<item quantity="other">% (mga) araw</item>
@ -811,7 +802,6 @@
<string name="manga_display_modified_interval_title">Itakdang i-update bawat</string>
<string name="pref_update_only_in_release_period">Sa labas ng inaasahang release period</string>
<string name="intervals_header">Mga pagitan</string>
<string name="pref_update_release_grace_period_info">Ang isang mababang palugit na panahon ay inirerekomenda upang mabawasan ang stress sa mga source. Kung mas maraming tseke para sa isang entry na-miss, mas mahaba ang pagitan sa pagitan ng mga tseke na may maximum na 28 araw.</string>
<string name="manga_modify_calculated_interval_title">I-customize ang Interval</string>
<string name="skipped_reason_not_in_release_period">Nilaktawan dahil walang inaasahang release ngayong araw</string>
<string name="has_results">May mga resulta</string>
@ -822,4 +812,6 @@
<string name="track_delete_remote_text">Tanggalin din mula sa %s</string>
<string name="syncing_library">Nagsi-sync ang aklatan</string>
<string name="library_sync_complete">Natapos na ang pag-sync ng aklatan</string>
<string name="information_cloudflare_help">I-tap dito para sa tulong sa Cloudflare</string>
<string name="download_cache_invalidated">Napawalang-bisa ang indise ng mga download</string>
</resources>

View File

@ -841,7 +841,6 @@
<string name="action_filter_interval_dropped">Abandonné \? Fin 20+ et 2 mois</string>
<string name="action_filter_interval_passed">Période de contrôle réussie</string>
<string name="action_sort_next_updated">Prochaine mise à jour prévue</string>
<string name="pref_update_release_grace_period">Délai de grâce prévu pour la publication</string>
<string name="pref_update_only_in_release_period">Période de diffusion prévue</string>
<string name="action_set_interval">Définir l\'intervalle</string>
<string name="action_ok">Valider</string>

View File

@ -211,7 +211,7 @@
<string name="cover_updated">कवर अपडेट किया गया</string>
<string name="chapter_progress">पृष्ठ: %1$d</string>
<string name="no_next_chapter">अगले अध्याय नहीं मिला</string>
<string name="decode_image_error">छवि को लोड नहीं कर पायें</string>
<string name="decode_image_error">छवि को लोड नहीं किया जा सका</string>
<string name="confirm_set_image_as_cover">कवर कला के रूप में इस छवि का उपयोग करें\?</string>
<string name="download_queue_error">अध्याय डाउनलोड नहीं कर सका। आप डाउनलोड अनुभाग में फिर से कोशिश कर सकते हैं</string>
<string name="notification_new_chapters">नए अध्याय पाए गए</string>
@ -279,7 +279,7 @@
<string name="transition_no_previous">कोई पिछला अध्याय नहीं है</string>
<string name="transition_pages_loading">पेज लोड हो रहे है …</string>
<string name="transition_pages_error">पृष्ठों को लोड करने में विफल है: %1$s</string>
<string name="pref_read_with_long_tap">संवाद के लिए लंबी प्रेस</string>
<string name="pref_read_with_long_tap">लंबे टैप पर क्रियाएँ दिखाएँ</string>
<string name="action_open_in_web_view">WebView में खोलें</string>
<string name="pref_true_color">32 बिट रंग</string>
<string name="pref_skip_read_chapters">पढ़े हुए अध्यायों को छोड़ें</string>
@ -552,7 +552,7 @@
<string name="pref_low">कम</string>
<string name="pref_lowest">निम्नतम</string>
<string name="restrictions">प्रतिबंध: %s</string>
<string name="automatic_background">स्वचालित</string>
<string name="automatic_background">ऑटो</string>
<string name="nav_zone_prev">पिछला</string>
<string name="nav_zone_next">अगला</string>
<string name="nav_zone_left">बाएं</string>
@ -618,7 +618,7 @@
<string name="update_72hour">हर 3 दिन</string>
<string name="connected_to_wifi">केवल वाई-फ़ाई पर</string>
<string name="clear_database_source_item_count">डेटाबेस में %1$d गैर-पुस्तकालय आइटम</string>
<string name="pref_auto_clear_chapter_cache">ऐप बंद करते समय चैप्टर कैशे साफ़ करें</string>
<string name="pref_auto_clear_chapter_cache">ऐप लॉन्च पर चैप्टर कैशे साफ़ करें</string>
<string name="database_clean">साफ़ करने के लिए कुछ नहीं है</string>
<string name="pref_update_only_completely_read">अपठित अध्याय हैं</string>
<string name="pref_library_update_manga_restriction">शीर्षक अद्यतन न करें</string>
@ -683,14 +683,14 @@
<string name="loader_rar5_error">RARv5 प्रारूप समर्थित नहीं है</string>
<string name="updates_last_update_info">पुस्तकालय पिछली बार अपडेट किया गया: %s</string>
<string name="appwidget_updates_description">अपनी हाल ही में अपडेट की गई पुस्तकालय एन्ट्री देखें</string>
<string name="download_ahead_info">केवल लाइब्रेरी में प्रविष्टियों पर काम करता है। और यदि वर्तमान अध्याय और अगला अध्याय पहले ही डाउनलोड हो चुका है</string>
<string name="download_ahead_info">केवल तभी काम करता है जब वर्तमान अध्याय + अगला पहले से ही डाउनलोड किया गया हो।</string>
<string name="custom_cover">कस्टम कवर</string>
<string name="pref_user_agent_string">चूक यूजर एजेंट स्ट्रिंग (User agent string)</string>
<string name="download_ahead">आगे डाउनलोड करें</string>
<string name="auto_download_while_reading">पढ़ते समय ऑटो डाउनलोड करे</string>
<plurals name="next_unread_chapters">
<item quantity="one">अगला अपठित अध्याय</item>
<item quantity="other">अगले अपठित अध्याय %d</item>
<item quantity="other">अगले %d अपठित अध्याय</item>
</plurals>
<string name="action_remove_everything">सब कुछ हटा दें</string>
<string name="popular">लोकप्रिय</string>
@ -732,4 +732,26 @@
<string name="action_filter_interval_long">मासिक प्राप्त करें (28 दिन)</string>
<string name="action_filter_interval_late">देर से 10+ की जाँच</string>
<string name="action_filter_interval_dropped">छोड़ा हुआ\? देर से 20+ और 2 महीने</string>
<string name="intervals_header">अंतराल</string>
<string name="pref_chapter_swipe_end">दाईं ओर स्वाइप करने पर</string>
<string name="pref_chapter_swipe">अध्याय स्वाइप</string>
<string name="action_sort_next_updated">अगला अपेक्षित अपडेट</string>
<string name="pref_debug_info">डीबग जानकारी</string>
<string name="pref_advanced_summary">डंप क्रैश लॉग, बैटरी अनुकूलन</string>
<string name="pref_update_only_in_release_period">अपेक्षित रिलीज़ अवधि से बाहर</string>
<string name="pref_chapter_swipe_start">बाईं ओर स्वाइप करने पर</string>
<string name="library_sync_complete">लाइब्रेरी सिंक पूरा</string>
<string name="download_cache_invalidated">डाउनलोड अनुक्रमणिका अमान्य</string>
<string name="action_ok">ठीक है</string>
<string name="pref_invalidate_download_cache">डाउनलोड अनुक्रमणिका अमान्य करें</string>
<plurals name="day">
<item quantity="one">1 दिन</item>
<item quantity="other">%d दिन</item>
</plurals>
<plurals name="download_amount">
<item quantity="one">अगला अध्याय</item>
<item quantity="other">अगले %d अध्याय</item>
</plurals>
<string name="copied_to_clipboard_plain">क्लिपबोर्ड पर कॉपी हो गया है</string>
<string name="track_delete_remote_text">%s से भी हटा दें</string>
</resources>

View File

@ -693,7 +693,7 @@
<string name="ext_info_age_rating">Dobna granica</string>
<string name="download_ahead">Preuzmi unaprijed</string>
<string name="multi_lang">Višejezičnost</string>
<string name="missing_storage_permission">Nedostaje dozvola za spremanje</string>
<string name="missing_storage_permission">Dozvole za spremanje nisu odobrena</string>
<string name="theme_tidalwave">Tsunami</string>
<string name="invalid_location">Nevažeća lokacija: %s</string>
<string name="pref_advanced_summary">Zapisnici iznenadnog gašenja aplikacije, optimizacije baterije</string>
@ -809,18 +809,8 @@
<string name="manga_modify_calculated_interval_title">Prilagodi interval</string>
<string name="action_sort_next_updated">Sljedeće očekivano aktualiziranje</string>
<string name="manga_display_modified_interval_title">Postavi za aktualiziranje svakih</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d dan nakon</item>
<item quantity="few">%d dana nakon</item>
<item quantity="other">%d dana nakon</item>
</plurals>
<string name="action_ok">U redu</string>
<string name="pref_update_only_in_release_period">Izvan očekivanog razdoblja izdavanja</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d dan prije</item>
<item quantity="few">%d dana prije</item>
<item quantity="other">%d dana prije</item>
</plurals>
<string name="intervals_header">Intervali</string>
<string name="track_delete_title">Ukloniti praćenje %s\?</string>
<string name="skipped_reason_not_in_release_period">Preskočeno, jer se danas nije očekivalo izdanje</string>
@ -829,9 +819,16 @@
<item quantity="few">%d dana</item>
<item quantity="other">%d dana</item>
</plurals>
<string name="pref_update_release_grace_period">Očekivano razdoblje odgode izdanja</string>
<string name="track_delete_text">Ovo će ukloniti lokalno praćenje.</string>
<string name="track_delete_remote_text">Također ukloni iz %s</string>
<string name="delete_downloaded">Izbriši preuzete</string>
<string name="has_results">Ima rezultate</string>
<string name="syncing_library">Sinkroniziranje biblioteke</string>
<string name="library_sync_complete">Sinkroniziranje biblioteke završeno</string>
<string name="information_cloudflare_help">Dodirni ovdje za pomoć s Cloudflareom</string>
<string name="action_filter_interval_dropped">Ispušteno\? Zadnjih 20 dana i 2 mjeseca</string>
<string name="track_activity_name">Zapis praćenja</string>
<string name="action_filter_interval_passed">Prekoraöeno razdoblje provjere</string>
<string name="action_filter_interval_late">Provjera zadnjih 10 i više dana</string>
<string name="download_cache_invalidated">Indeks preuzimanja poništen</string>
</resources>

View File

@ -772,9 +772,6 @@
<string name="pref_chapter_swipe_start">Geser kekiri</string>
<string name="pref_double_tap_zoom">Ketuk dua kali untuk memperbesar</string>
<string name="pref_library_columns_per_row">%d per baris</string>
<plurals name="pref_update_release_following_days">
<item quantity="other">%d hari setelahnya</item>
</plurals>
<plurals name="day">
<item quantity="other">%d hari</item>
</plurals>
@ -782,18 +779,13 @@
<string name="action_filter_interval_custom">Interval pengambilan disesuaikan</string>
<string name="action_filter_interval_long">Ambil bulanan (28 hari)</string>
<string name="action_filter_interval_late">Cek 10+ terlambat</string>
<string name="pref_update_release_grace_period">Masa tenggang rilis yang diharapkan</string>
<string name="manga_modify_calculated_interval_title">Sesuaikan Interval</string>
<string name="skipped_reason_not_in_release_period">Dilewati karena tidak ada rilis yang diharapkan hari ini</string>
<string name="action_filter_interval_dropped">Berkurang\? Akhir 20+ dan 2 bulan</string>
<string name="action_filter_interval_passed">Melewati periode pemeriksaan</string>
<string name="action_sort_next_updated">Pembaruan yang diharapkan berikutnya</string>
<string name="pref_update_release_grace_period_info">Masa tenggang rendah disarankan untuk meminimalkan stres pada sumber. Semakin banyak pemeriksaan untuk entri yang terlewatkan, semakin lama interval antar pemeriksaan dengan maksimal 28 hari.</string>
<string name="intervals_header">Interval</string>
<string name="pref_update_only_in_release_period">Di luar periode rilis yang diharapkan</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">%d hari sebelumnya</item>
</plurals>
<string name="manga_display_interval_title">Perkirakan setiap</string>
<string name="manga_display_modified_interval_title">Atur untuk memperbarui setiap</string>
<string name="track_delete_title">Hapus %s pelacakan\?</string>
@ -804,4 +796,7 @@
<string name="has_results">Memiliki hasil</string>
<string name="syncing_library">Sinkronisasi pustaka</string>
<string name="library_sync_complete">Sinkronisasi pustaka selesai</string>
<string name="information_cloudflare_help">Ketuk di sini untuk bantuan dengan Cloudflare</string>
<string name="download_cache_invalidated">Indeks unduhan tidak valid</string>
<string name="track_activity_name">Pelacak login</string>
</resources>

View File

@ -822,7 +822,7 @@
<string name="action_update_category">Aggiorna categoria</string>
<string name="split_tall_images">Dividi immagini alte</string>
<string name="overlay_header">Sovrimpressione</string>
<string name="pref_page_rotate">Ruota le pagine larghe per adattareaallo schermo</string>
<string name="pref_page_rotate">Ruota le pagine larghe per adattarle allo schermo</string>
<string name="pref_page_rotate_invert">Capovolgi l\'orientamento delle pagine larghe ruotate</string>
<plurals name="missing_chapters">
<item quantity="one">Manca %1$s capitolo</item>
@ -839,17 +839,6 @@
<string name="action_filter_interval_long">Recupera mensilmente (28 giorni)</string>
<string name="action_filter_interval_dropped">Abbandonato\? In ritardo tra 20 giorni e 2 mesi</string>
<string name="pref_update_only_in_release_period">Fuori dal periodo di rilascio previsto</string>
<string name="pref_update_release_grace_period">Periodo di grazia previsto per l\'uscita</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d giorno prima</item>
<item quantity="many">%d giorni prima</item>
<item quantity="other">%d giorni prima</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d giorno dopo</item>
<item quantity="many">%d giorni dopo</item>
<item quantity="other">%d giorni dopo</item>
</plurals>
<string name="manga_modify_calculated_interval_title">Personalizza intervallo</string>
<string name="intervals_header">Intervalli</string>
<plurals name="day">
@ -862,7 +851,6 @@
<string name="action_filter_interval_passed">Periodo di controllo superato</string>
<string name="action_filter_interval_late">In ritardo di 10+ giorni</string>
<string name="action_sort_next_updated">Prossimo aggiornamento previsto</string>
<string name="pref_update_release_grace_period_info">Si consiglia un periodo di grazia basso per ridurre al minimo lo stress sulle fonti. Più controlli per una voce vengono persi, più lungo sarà l\'intervallo tra i controlli, con un massimo di 28 giorni.</string>
<string name="manga_display_modified_interval_title">Imposta l\'aggiornamento ogni</string>
<string name="skipped_reason_not_in_release_period">Saltato perché oggi non era previsto alcun rilascio</string>
<string name="track_delete_title">Rimuovere il tracking di %s\?</string>
@ -874,9 +862,6 @@
<string name="library_sync_complete">Sincronizzazione libreria completata</string>
<string name="syncing_library">Sincronizzazione libreria</string>
<string name="information_cloudflare_help">Tocca qua per assistenza con Cloudflare</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d giorni</item>
<item quantity="many">%1$d - %2$d giorni</item>
<item quantity="other">%1$d - %2$d giorni</item>
</plurals>
<string name="download_cache_invalidated">Indice dei download invalidato</string>
<string name="track_activity_name">Login del tracking</string>
</resources>

View File

@ -776,7 +776,6 @@
<string name="action_filter_interval_custom">カスタマイズした取得間隔</string>
<string name="action_filter_interval_long">月一回に取得28日</string>
<string name="action_sort_next_updated">次の更新予定</string>
<string name="pref_update_release_grace_period">予想された更新猶予時間</string>
<string name="pref_update_only_in_release_period">更新予定時間外</string>
<string name="intervals_header">間隔</string>
<plurals name="day">
@ -787,10 +786,6 @@
<string name="has_results">実績あり</string>
<string name="track_delete_title">%s の追跡を削除しますか\?</string>
<string name="manga_display_interval_title">毎に評価</string>
<string name="pref_update_release_grace_period_info">ソースの負担を軽減するため、少しの猶予時間を設定しておくことをおすすめします。チェック時に更新が見つからない場合、チェック間の間隔が自動で最大28日まで増えられます。</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">%d 日前</item>
</plurals>
<string name="manga_display_modified_interval_title">ごとに更新するように設定する</string>
<string name="track_delete_text">ローカルの追跡が削除されます。</string>
<string name="track_delete_remote_text">%s からも削除</string>
@ -798,10 +793,9 @@
<string name="action_filter_interval_late">10+チェック後半</string>
<string name="action_filter_interval_dropped">落とした\? 20歳以上後半と2ヶ月</string>
<string name="action_filter_interval_passed">チェック期間を過ぎました</string>
<plurals name="pref_update_release_following_days">
<item quantity="other">%d日後</item>
</plurals>
<string name="action_ok">OK</string>
<string name="syncing_library">ライブラリを同期しています</string>
<string name="library_sync_complete">ライブラリを同期しました</string>
<string name="information_cloudflare_help">Cloudflareに関するヘルプ情報はこちら</string>
<string name="download_cache_invalidated">ダウンロード インデックスを消去しました</string>
</resources>

View File

@ -778,20 +778,18 @@
<string name="action_set_interval">간격 설정</string>
<string name="action_filter_interval_custom">사용자 지정 가져오기 간격</string>
<string name="action_filter_interval_long">매월 가져오기 (28일)</string>
<plurals name="pref_update_release_following_days">
<item quantity="other">%d일 후</item>
</plurals>
<string name="intervals_header">간격</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">%d일 전</item>
</plurals>
<string name="manga_modify_calculated_interval_title">간격 설정</string>
<string name="delete_downloaded">다운로드 삭제</string>
<string name="has_results">결과가 있는 것만 보기</string>
<string name="track_delete_text">이렇게 하면 로컬에서 동기화가 제거됩니다.</string>
<string name="track_delete_remote_text">%s에서도 제거</string>
<string name="pref_update_release_grace_period">예상 업데이트 유예 기간</string>
<string name="pref_update_release_grace_period_info">소스에 대한 부하를 최소화하려면 낮은 유예 기간을 권장합니다. 누락된 항목에 대한 확인 횟수가 많을수록 확인 간격이 최대 28일로 길어집니다.</string>
<string name="action_ok">오케이</string>
<string name="action_ok">OK</string>
<string name="skipped_reason_not_in_release_period">오늘 연재가 예상되지 않았기 때문에 건너뛰었습니다</string>
<string name="track_delete_title">%s 동기화를 삭제 하시겠습니까\?</string>
<string name="action_filter_interval_passed">체크 기간이 지났습니다</string>
<string name="pref_update_only_in_release_period">연재 예정 기간 제외</string>
<string name="download_cache_invalidated">다운로드 인덱스를 제거함</string>
<string name="action_sort_next_updated">다음 업데이트 예정</string>
<string name="information_cloudflare_help">탭하여 Cloudflare에 관한 도움말 보기</string>
</resources>

View File

@ -778,12 +778,8 @@
<string name="action_filter_interval_late">Lewat semak 10+</string>
<string name="action_filter_interval_dropped">Diabaikan\? Lewat 20+ dan 2 bulan</string>
<string name="action_sort_next_updated">Kemas kini seterusnya dijangka</string>
<plurals name="pref_update_release_following_days">
<item quantity="other">%d hari selepasnya</item>
</plurals>
<string name="manga_display_modified_interval_title">Tetapkan untuk kemas kini setiap</string>
<string name="skipped_reason_not_in_release_period">Dilangkau kerana tiada keluaran yang dijangkakan hari ini</string>
<string name="pref_update_release_grace_period">Dalam jangkaan tempoh tangguh keluaran</string>
<string name="intervals_header">Jarak masa</string>
<plurals name="day">
<item quantity="other">%d hari</item>
@ -791,10 +787,6 @@
<string name="manga_modify_calculated_interval_title">Tersuai Jarak Masa</string>
<string name="pref_update_only_in_release_period">Diluar jangkaan masa keluaran</string>
<string name="manga_display_interval_title">Anggaran setiap</string>
<string name="pref_update_release_grace_period_info">Tempoh tangguh yang rendah adalah digalakkan untuk meminimumkan tekanan kepada sumber. Lebih banyak semakan pada entri yang terlepas, semakin lama jarak masa akan diambil untuk menyemak dengan 28 hari maksimum.</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">%d hari sebelumnya</item>
</plurals>
<string name="action_filter_interval_passed">Melepasi tempoh semak</string>
<string name="track_delete_title">Buang penjejakan %s\?</string>
<string name="track_delete_text">Ini akan membuang penjejakan secara lokal.</string>

View File

@ -798,20 +798,10 @@
<string name="action_sort_next_updated">Neste forventede oppdatering</string>
<string name="action_filter_interval_dropped">Droppet\? Sen 20+ og 2 måneder</string>
<string name="pref_chapter_swipe_end">Sveip til høyre handling</string>
<string name="pref_update_release_grace_period">Forventet utgivelsestoleranse</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d dag før</item>
<item quantity="other">%d dager før</item>
</plurals>
<string name="intervals_header">Intervaller</string>
<string name="manga_display_interval_title">Anslå hver</string>
<string name="delete_downloaded">Slett nedlastede</string>
<string name="pref_update_only_in_release_period">Utenfor forventet utgivelsesperiode</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d dag etter</item>
<item quantity="other">%d dager etter</item>
</plurals>
<string name="pref_update_release_grace_period_info">En kort utgivelsestoleranse anbefales for å minimere belastningen på kildene. Jo flere sjekker som går tapt, desto lengre blir intervallet mellom sjekkene, med en maksimal periode på 28 dager.</string>
<string name="pref_double_tap_zoom">Dobbelttrykk for å zoome</string>
<string name="pref_chapter_swipe_start">Sveip til venstre handling</string>
<string name="track_delete_title">Vil du fjerne sporing for %s\?</string>
@ -822,9 +812,6 @@
<string name="has_results">Har resultater</string>
<string name="syncing_library">Synkroniserer biblioteket</string>
<string name="library_sync_complete">Biblioteksynkronisering fullført</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d dag</item>
<item quantity="other">%1$d - %2$d dager</item>
</plurals>
<string name="information_cloudflare_help">Trykk her for å få hjelp med Cloudflare</string>
<string name="download_cache_invalidated">Nedlastingsindeksen er ugyldiggjort</string>
</resources>

View File

@ -790,18 +790,9 @@
<string name="action_set_interval">अन्तराल सेट गर्नुहोस्</string>
<string name="action_filter_interval_custom">कस्टम गरिएको ल्याउने अन्तराल</string>
<string name="action_filter_interval_long">मासिक ल्याउनुहोस् (२८ दिन)</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d दिन अघि</item>
<item quantity="other">%d दिन अघि</item>
</plurals>
<string name="pref_update_release_grace_period">अपेक्षित रिलीज ग्रेस अवधि</string>
<string name="manga_display_modified_interval_title">प्रत्येक अपडेट गर्न सेट गर्नुहोस्</string>
<string name="manga_modify_calculated_interval_title">अन्तराल कस्टम गर्नुहोस्</string>
<string name="skipped_reason_not_in_release_period">छोडियो किनभने आज कुनै रिलीज अपेक्षित थिएन</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d दिन पछि</item>
<item quantity="other">%d दिन पछि</item>
</plurals>
<string name="intervals_header">अन्तरालहरू</string>
<plurals name="day">
<item quantity="one">१ दिन</item>
@ -811,7 +802,6 @@
<string name="action_filter_interval_passed">जाँच अवधि पार भयो</string>
<string name="action_sort_next_updated">अर्को अपेक्षित अपडेट</string>
<string name="pref_update_only_in_release_period">अपेक्षित रिलीज अवधि बाहिर</string>
<string name="pref_update_release_grace_period_info">स्रोतहरूमा तनाव कम गर्न छोटो ग्रेस अवधिको साथ अन्तराल सिफारिस गरिन्छ। छुटेको इन्ट्रीको लागि जति धेरै जाँचहरू छन्, जाँचहरू बीचको अन्तर त्यति नै लामो हुन्छ, अधिकतम २८ दिनको साथ।</string>
<string name="manga_display_interval_title">प्रत्येक अनुमान लगाउनुहोस्</string>
<string name="action_filter_interval_dropped">छोडियो\? ढिलो २०+ र २ महिना</string>
<string name="track_delete_title">%s ट्र्याकिङ हटाउने हो\?</string>
@ -820,4 +810,8 @@
<string name="action_ok">ठीक छ</string>
<string name="delete_downloaded">डाउनलोड गरिएको मेट्नुहोस्</string>
<string name="has_results">परिणामहरू छन्</string>
<string name="library_sync_complete">पुस्तकालय सिङ्क सम्पन्न भयो</string>
<string name="syncing_library">पुस्तकालय सिङ्क गर्दै</string>
<string name="download_cache_invalidated">डाउनलोड इन्डेक्स अवैध भयो</string>
<string name="information_cloudflare_help">Cloudflare सम्बन्धित मद्दतको लागि यहाँ ट्याप गर्नुहोस्</string>
</resources>

View File

@ -347,7 +347,7 @@
<string name="add_to_library">Dodaj do biblioteki</string>
<string name="http_error_hint">Sprawdź stronę w WebView</string>
<string name="email">Adres e-mail</string>
<string name="downloaded_only_summary">Filtruje wszystkie wpisy w twojej bibliotece</string>
<string name="downloaded_only_summary">Filtruje wszystkie pozycje w twojej bibliotece</string>
<string name="label_downloaded_only">Tylko pobrane</string>
<string name="check_for_updates">Sprawdź aktualizacje</string>
<string name="licenses">Licencje open source</string>
@ -641,7 +641,7 @@
<string name="channel_app_updates">Aktualizacje aplikacji</string>
<string name="ext_update_all">Zaktualizuj wszystko</string>
<string name="ext_installer_legacy">Przestarzały</string>
<string name="pref_auto_clear_chapter_cache">Wyczyść cache rozdziałów przy wychodzeniu z aplikacji</string>
<string name="pref_auto_clear_chapter_cache">Wyczyść cache rozdziałów przy uruchamianiu aplikacji</string>
<string name="extension_api_error">Nie można uzyskać listy rozszerzeń</string>
<string name="publishing_finished">Opublikowane w całości</string>
<string name="database_clean">Nie ma nic do wyczyszczynia</string>
@ -725,14 +725,14 @@
<item quantity="other">Następne %d nieprzeczytanych rozdziałów</item>
</plurals>
<string name="are_you_sure">Jesteś pewien\?</string>
<string name="download_ahead_info">Działa tylko na wpisach w bibliotece oraz jeśli aktualny rozdział i następny są już pobrane</string>
<string name="download_ahead_info">Działa tylko, jeśli aktualny i następny rozdział są już pobrane.</string>
<string name="multi_lang">Wielojęzyczne</string>
<string name="pref_long_strip_split">Dziel wysokie obrazy (BETA)</string>
<string name="popular">Popularne</string>
<string name="updates_last_update_info">Biblioteka ostatnio aktualizowana: %s</string>
<string name="remove_manga">Zamierzasz usunąć \"%s\" ze swojej biblioteki</string>
<string name="label_stats">Statystyki</string>
<string name="label_started">Początek</string>
<string name="label_started">Rozpoczęte</string>
<string name="label_local">Lokalne</string>
<string name="label_downloaded">Pobrane</string>
<string name="pref_invalidate_download_cache_summary">Wymuś ponowne sprawdzenie pobranych rozdziałów przez aplikację</string>
@ -752,7 +752,7 @@
<string name="label_titles_in_global_update">W globalnej aktualizacji</string>
<string name="label_tracker_section">Śledzenie</string>
<string name="unknown_title">Nieznany tytuł</string>
<string name="updates_last_update_info_just_now">Właśnie teraz</string>
<string name="updates_last_update_info_just_now">Przed chwilą</string>
<string name="crash_screen_title">Ups!</string>
<string name="track_remove_date_conf_title">Usunąć datę\?</string>
<string name="track_remove_finish_date_conf_text">To spowoduje usunięcie wcześniej wybranej daty zakończenia z %s</string>
@ -761,24 +761,24 @@
<string name="action_not_now">Nie teraz</string>
<string name="label_overview_section">Przegląd</string>
<string name="label_completed_titles">Zakończone wpisy</string>
<string name="label_total_chapters">Całość</string>
<string name="label_total_chapters">Razem</string>
<string name="action_search_hint">Szukaj…</string>
<string name="action_open_random_manga">Otwórz losową pozycję</string>
<string name="pref_library_summary">Kategorie, aktualizacja globalna, przesunięcie rozdziału</string>
<string name="pref_invalidate_download_cache">Unieważnij indeks pobierania</string>
<string name="fdroid_warning">Kompilacje F-Droid nie są oficjalnie obsługiwane.
\nNaciśnij, aby dowiedzieć się więcej.</string>
<string name="label_read_chapters">Czytaj</string>
<string name="label_read_chapters">Przeczytane</string>
<string name="pref_downloads_summary">Automatyczne pobieranie, pobierz wstępnie</string>
<string name="track_remove_start_date_conf_text">To spowoduje usunięcie wcześniej wybranej daty rozpoczęcia z %s</string>
<string name="pref_library_update_show_tab_badge">Pokaż liczbę nieprzeczytanych na ikonie Aktualizacji</string>
<string name="not_applicable">N/A</string>
<string name="day_short">%dd</string>
<string name="hour_short">%d godz</string>
<string name="hour_short">%d godz.</string>
<string name="minute_short">%d minut</string>
<string name="seconds_short">%d sekund</string>
<string name="label_used">Używane</string>
<string name="label_tracked_titles">Śledzone wpisy</string>
<string name="label_tracked_titles">Śledzone pozycje</string>
<string name="label_mean_score">Średnia ocena</string>
<string name="skipped_reason_not_always_update">Pominięto, ponieważ aktualizacja nie jest wymagana</string>
<string name="information_no_entries_found">Nie znaleziono wpisów w tej kategorii</string>
@ -819,23 +819,12 @@
<string name="pref_chapter_swipe">Przesunięcie rozdziału</string>
<string name="action_set_interval">Ustaw interwał</string>
<string name="action_sort_next_updated">Następna spodziewana aktualizacja</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d dzień po</item>
<item quantity="few">%d dni po</item>
<item quantity="many">%d dni po</item>
<item quantity="other">%d dni po</item>
</plurals>
<string name="manga_display_modified_interval_title">Aktualizuj co</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d dzień przed</item>
<item quantity="few">%d dni przed</item>
<item quantity="many">%d dni przed</item>
<item quantity="other">%d dni przed</item>
</plurals>
<plurals name="day">
<item quantity="one">1 dzień</item>
<item quantity="few">%d dni</item>
<item quantity="many">%d dni</item>
<item quantity="other">%d dni</item>
</plurals>
<string name="syncing_library">Synchronizowanie biblioteki</string>
</resources>

View File

@ -810,7 +810,6 @@
<string name="action_filter_interval_long">Requisitar mensalmente (28 dias)</string>
<string name="action_filter_interval_dropped">Abandonado\? Últimos 20+ e 2 meses</string>
<string name="action_filter_interval_passed">Intervalo de verificação aprovado</string>
<string name="pref_update_release_grace_period">Intervalo de lançamento com carência</string>
<string name="manga_modify_calculated_interval_title">Personalizar intervalo</string>
<plurals name="day">
<item quantity="one">%d dia(s)</item>
@ -820,31 +819,16 @@
<string name="skipped_reason_not_in_release_period">Pulado porque nenhum lançamento era esperado hoje</string>
<string name="intervals_header">Intervalos</string>
<string name="manga_display_modified_interval_title">Definido para atualizar todo</string>
<string name="pref_update_release_grace_period_info">Um intervalo com uma carência pequena é recomendado para minimizar o estresse nas fontes. Quanto mais verificações para um item forem perdidas, maior será o intervalo entre as verificações, com um máximo de 28 dias.</string>
<string name="manga_display_interval_title">Estimar todo</string>
<string name="action_ok">OK</string>
<string name="track_delete_title">Remover o monitoramento do %s\?</string>
<string name="track_delete_text">Isso irá remover o monitoramento localmente.</string>
<string name="track_delete_remote_text">Também remover do %s</string>
<string name="delete_downloaded">Deletar os disponíveis offline</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d dia(s) antes</item>
<item quantity="many">%d dias antes</item>
<item quantity="other">%d dias antes</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d dia(s) depois</item>
<item quantity="many">%d dias depois</item>
<item quantity="other">%d dias depois</item>
</plurals>
<string name="has_results">Há resultados</string>
<string name="syncing_library">Sincronizando a biblioteca</string>
<string name="library_sync_complete">Sincronização da biblioteca finalizada</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d -%2$d dia(s)</item>
<item quantity="many">%1$d -%2$d dias</item>
<item quantity="other">%1$d -%2$d dias</item>
</plurals>
<string name="information_cloudflare_help">Toque aqui para obter ajuda com o Cloudflare</string>
<string name="download_cache_invalidated">Índice de downloads invalidado</string>
<string name="track_activity_name">Login do monitoramento</string>
</resources>

View File

@ -832,7 +832,6 @@
<string name="action_set_interval">Definir intervalo</string>
<string name="action_filter_interval_long">Buscar mensalmente (28 dias)</string>
<string name="pref_update_only_in_release_period">Fora do período esperado de lançamento</string>
<string name="pref_update_release_grace_period">Período de tolerância de liberação esperado</string>
<string name="pref_debug_info">Informações de depuração</string>
<plurals name="day">
<item quantity="one">Um dia</item>
@ -848,14 +847,4 @@
<string name="action_sort_next_updated">Próxima atualização esperada</string>
<string name="manga_display_modified_interval_title">Definido para atualizar a cada</string>
<string name="skipped_reason_not_in_release_period">Pulado, pois nenhum lançamento é esperado para hoje</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d dia depois</item>
<item quantity="many">%d dias depois</item>
<item quantity="other">%d dias depois</item>
</plurals>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d dia atrás</item>
<item quantity="many">%d dias atrás</item>
<item quantity="other">%d dias atrás</item>
</plurals>
</resources>

View File

@ -820,19 +820,6 @@
<string name="action_filter_interval_custom">Настраиваемый интервал получения</string>
<string name="action_filter_interval_long">Месячное получение (28 дней)</string>
<string name="action_filter_interval_dropped">Заброшено\? Прошлые 20+ дней и 2 месяца</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">через %d день</item>
<item quantity="few">через %d дня</item>
<item quantity="many">через %d дней</item>
<item quantity="other">через %d дней</item>
</plurals>
<string name="pref_update_release_grace_period">Внутри предела ожидаемого периода выпуска</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">за %d день до</item>
<item quantity="few">за %d дня до</item>
<item quantity="many">за %d дней до</item>
<item quantity="other">за %d дней до</item>
</plurals>
<string name="intervals_header">Интервалы</string>
<string name="manga_display_interval_title">Оценивать каждые</string>
<string name="manga_modify_calculated_interval_title">Настроить интервал</string>
@ -842,7 +829,6 @@
<string name="action_sort_next_updated">Следующее ожидамое обновление</string>
<string name="pref_update_only_in_release_period">За пределами ожидаемого периода выпуска</string>
<string name="manga_display_modified_interval_title">Задать обновления каждые</string>
<string name="pref_update_release_grace_period_info">Следует использовать низкий льготный период, чтобы минимизировать нагрузку на источники. Чем больше проверок серий, которые были пропущены, тем длиннее интервал между проверками в сумме 28 дней.</string>
<plurals name="day">
<item quantity="one">1 день</item>
<item quantity="few">%d дня</item>
@ -859,10 +845,5 @@
<string name="library_sync_complete">Синхронизация библиотеки завершена</string>
<string name="syncing_library">Синхронизация библиотеки</string>
<string name="information_cloudflare_help">Нажмите здесь, чтобы получить помощь с Cloudflare</string>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d день</item>
<item quantity="few">%1$d - %2$d дня</item>
<item quantity="many">%1$d - %2$d дней</item>
<item quantity="other">%1$d - %2$d дней</item>
</plurals>
<string name="download_cache_invalidated">Индекс загрузок недействителен</string>
</resources>

View File

@ -800,17 +800,7 @@
<string name="action_filter_interval_late">Verìfica in ritardu de 10+ dies</string>
<string name="action_filter_interval_passed">Perìodu de controllu coladu</string>
<string name="manga_modify_calculated_interval_title">Personaliza s\'intervallu</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d die in antis</item>
<item quantity="other">%d dies in antis</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d die a pustis</item>
<item quantity="other">%d dies a pustis</item>
</plurals>
<string name="pref_update_release_grace_period_info">Unu perìodu de gràtzia bassu est cussigiadu pro minimare sa pressione subra sas fontes. Prus controllos pro un\'elementu si perdent, prus longu at a èssere s\'intervallu intre sos controllos cun unu màssimu de 28 dies.</string>
<string name="action_ok">AB</string>
<string name="pref_update_release_grace_period">Perìodu de gràtzia prevìdidu pro sa publicatzione</string>
<string name="track_delete_title">Bogare s\'arrastadore de %s\?</string>
<string name="track_delete_text">Custu at a bogare s\'arrastamentu locale.</string>
<string name="track_delete_remote_text">Boga fintzas dae %s</string>

View File

@ -70,7 +70,7 @@
<string name="pref_category_tracking">Spårning</string>
<string name="pref_category_advanced">Avancerat</string>
<string name="pref_category_about">Om appen</string>
<string name="pref_library_columns">Artiklar per rad</string>
<string name="pref_library_columns">Rutnätets storlek</string>
<string name="portrait">Porträtt</string>
<string name="landscape">Landskap</string>
<string name="pref_library_update_interval">Automatiska uppdateringar</string>
@ -117,10 +117,10 @@
<string name="white_background">Vit</string>
<string name="black_background">Svart</string>
<string name="pref_viewer_type">Standardläsläge</string>
<string name="left_to_right_viewer">Vänster till höger</string>
<string name="right_to_left_viewer">Höger till vänster</string>
<string name="vertical_viewer">Vertikal</string>
<string name="webtoon_viewer">Webtoon</string>
<string name="left_to_right_viewer">Sidor (vänster till höger)</string>
<string name="right_to_left_viewer">Sidor (höger till vänster)</string>
<string name="vertical_viewer">Sidformat (vertikalt)</string>
<string name="webtoon_viewer">Lång remsa</string>
<string name="pager_viewer">Sidläsare</string>
<string name="pref_image_scale_type">Bildanpassning</string>
<string name="scale_type_fit_screen">Passa skärmen</string>
@ -282,7 +282,7 @@
<string name="action_open_in_web_view">Öppna i WebView</string>
<string name="pref_true_color">32-bitars färg</string>
<string name="pref_skip_read_chapters">Hoppa över lästa kapitel</string>
<string name="pref_read_with_long_tap">Visa vid lång tryckning</string>
<string name="pref_read_with_long_tap">Visa åtgärder vid lång tryckning</string>
<string name="pref_color_filter_mode">Färgfilterblandningsläge</string>
<string name="filter_mode_overlay">Överlägg</string>
<string name="filter_mode_multiply">Multiplicera</string>
@ -373,7 +373,7 @@
<string name="add_to_library">Lägg till i biblioteket</string>
<string name="pinned_sources">Nålad</string>
<string name="pref_webtoon_side_padding">Sidofyllning</string>
<string name="vertical_plus_viewer">Kontinuerlig vertikal</string>
<string name="vertical_plus_viewer">Lång remsa med mellanrum</string>
<string name="action_unpin">Lossa</string>
<string name="action_pin">Nåla</string>
<string name="action_select_inverse">Välj omvänd</string>
@ -561,7 +561,7 @@
<string name="cover_saved">Omslaget har sparats</string>
<string name="manga_cover">Omslag</string>
<string name="tracking_guide">Spårningsguide</string>
<string name="categorized_display_settings">Inställningar per kategori för sortering och visning</string>
<string name="categorized_display_settings">Inställningar per kategori för sortering</string>
<string name="information_empty_category_dialog">Du har inga kategorier ännu.</string>
<string name="action_start_downloading_now">Börja ladda ner nu</string>
<string name="notification_updating">Uppdaterar biblioteket ... (%1$d / %2$d)</string>
@ -617,7 +617,7 @@
<string name="update_72hour">Var 3:e dag</string>
<string name="ext_update_all">Uppdatera alla</string>
<string name="channel_app_updates">Appuppdateringar</string>
<string name="pref_auto_clear_chapter_cache">Rensa kapitelcache när appen stängs</string>
<string name="pref_auto_clear_chapter_cache">Rensa kapitelcache när appen startas</string>
<string name="clear_database_source_item_count">%1$d poster i databasen som inte är biblioteksposter</string>
<string name="database_clean">Inget att rensa</string>
<string name="library_errors_help">För hjälp med att åtgärda fel i biblioteksuppdateringar, se %1$s</string>
@ -633,8 +633,8 @@
<string name="action_show_manga">Visa inlägg</string>
<string name="action_display_cover_only_grid">Endast omslags-rutnät</string>
<string name="pref_update_only_started">Som inte har startats</string>
<string name="pref_navigate_pan">Panorera breda sidor med tryck</string>
<string name="pref_landscape_zoom">Zooma in landskapsbilden</string>
<string name="pref_navigate_pan">Panorera breda sidor</string>
<string name="pref_landscape_zoom">Automatiskt zoomning i stora bilder</string>
<string name="skipped_reason_completed">Hoppade över eftersom serien är klar</string>
<string name="skipped_reason_not_caught_up">Hoppade över eftersom det finns olästa kapitel</string>
<string name="skipped_reason_not_started">Hoppade över eftersom inga kapitel läses</string>
@ -702,10 +702,10 @@
<string name="remove_manga">Du är på väg att ta bort \"%s\" från ditt bibliotek</string>
<string name="download_ahead">Ladda ner i förväg</string>
<string name="theme_tidalwave">Tidvattenvåg</string>
<string name="download_ahead_info">Fungerar endast för poster i biblioteket och om det aktuella kapitlet och nästa kapitel redan har laddats ner</string>
<string name="download_ahead_info">Fungerar endast om det aktuella kapitlet + nästa redan har laddats ner.</string>
<string name="pref_long_strip_split">Dela stora bilder (BETA)</string>
<string name="auto_download_while_reading">Automatisk nedladdning under läsning</string>
<string name="pref_library_summary">Kategorier, global uppdatering</string>
<string name="pref_library_summary">Kategorier, global uppdatering, kapitel svepning</string>
<string name="pref_reader_summary">Läsläge, skärmvisning, navigering</string>
<string name="error_user_agent_string_invalid">Ogiltig sträng för användaragent</string>
<string name="unknown_title">Okänd titel</string>
@ -751,4 +751,67 @@
<string name="pref_page_rotate">Rotera breda sidor så att de passar</string>
<string name="pref_page_rotate_invert">Vänd orientering av roterade breda sidor</string>
<string name="information_required_plain">*krävs</string>
<string name="information_no_entries_found">Inga inlägg hittades i denna kategori</string>
<string name="label_mean_score">Genomsnittlig poäng</string>
<string name="minute_short">%dm</string>
<string name="seconds_short">%ds</string>
<plurals name="day">
<item quantity="one">1 dag</item>
<item quantity="other">%d dagar</item>
</plurals>
<string name="label_used">Använd</string>
<string name="not_applicable">N/A</string>
<string name="confirm_add_duplicate_manga">Du har en post i ditt bibliotek med samma namn.
\n
\nVill du fortfarande fortsätta\?</string>
<string name="track_remove_date_conf_title">Ta bort datum\?</string>
<string name="label_titles_section">Inlägg</string>
<string name="label_titles_in_global_update">I global uppdatering</string>
<string name="label_total_chapters">Totalt</string>
<string name="label_read_chapters">Läst</string>
<string name="manga_display_interval_title">Uppskatta varje</string>
<string name="action_set_interval">Ange intervall</string>
<string name="action_filter_interval_custom">Anpassat hämtningsintervall</string>
<string name="action_filter_interval_long">Hämta månadsvis (28 dagar)</string>
<string name="action_filter_interval_late">Sen 10+ check</string>
<string name="action_sort_next_updated">Nästa förväntade uppdatering</string>
<string name="skipped_reason_not_in_release_period">Hoppades över eftersom ingen publicering förväntades idag</string>
<string name="label_tracked_titles">Spårade inlägg</string>
<string name="day_short">%dd</string>
<string name="manga_modify_calculated_interval_title">Anpassa intervall</string>
<plurals name="download_amount">
<item quantity="one">Nästa kapitel</item>
<item quantity="other">Nästa %d kapitel</item>
</plurals>
<string name="track_error">%1$s fel: %2$s</string>
<string name="action_filter_interval_dropped">Avhoppad\? Sen 20+ och 2 månader</string>
<string name="action_filter_interval_passed">Godkänd kontrollperiod</string>
<string name="pref_double_tap_zoom">Dubbeltryck för att zooma</string>
<string name="track_remove_start_date_conf_text">Detta kommer att ta bort ditt tidigare valda startdatum från %s</string>
<string name="track_delete_title">Ta bort %s spårning\?</string>
<string name="track_delete_text">Detta kommer att ta bort spårningen lokalt.</string>
<string name="track_remove_finish_date_conf_text">Detta kommer att ta bort ditt tidigare valda slutdatum från %s</string>
<string name="track_delete_remote_text">Ta även bort från %s</string>
<string name="information_no_manga_category">Kategorin är tom</string>
<string name="hour_short">%dt</string>
<string name="syncing_library">Synkronisering av bibliotek</string>
<string name="pref_debug_info">Felsökningsinformation</string>
<string name="copied_to_clipboard_plain">Kopierad till urklipp</string>
<string name="manga_display_modified_interval_title">Ställ in för att uppdatera varje</string>
<string name="has_results">Har resultat</string>
<string name="label_completed_titles">Avslutade inlägg</string>
<string name="pref_library_columns_per_row">%d per rad</string>
<string name="library_sync_complete">Bibliotekssynkronisering slutförd</string>
<string name="download_cache_invalidated">Index för nedladdningar ogiltigt</string>
<string name="label_overview_section">Översikt</string>
<string name="delete_downloaded">Radera nedladdat</string>
<string name="pref_chapter_swipe">Kapitel svepning</string>
<string name="pref_chapter_swipe_end">Svep till höger åtgärd</string>
<string name="pref_chapter_swipe_start">Svep till vänster åtgärd</string>
<string name="label_read_duration">Lästid</string>
<string name="label_tracker_section">Spårare</string>
<string name="information_cloudflare_help">Klicka här för hjälp med Cloudflare</string>
<string name="action_ok">OK</string>
<string name="pref_update_only_in_release_period">Utanför förväntad releaseperiod</string>
<string name="intervals_header">Intervaller</string>
</resources>

View File

@ -780,20 +780,12 @@
<string name="action_filter_interval_passed">การตรวจสอบที่ผ่านมา</string>
<string name="action_sort_next_updated">การอัปเดตที่คาดไว้ต่อไป</string>
<string name="pref_update_only_in_release_period">ระยะเวลาการออกที่คาดไว้จากภายนอก</string>
<string name="pref_update_release_grace_period">ระยะเวลาการเลื่อนออกที่คาดไว้</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">%d วันก่อน</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="other">อีก %d วัน</item>
</plurals>
<plurals name="day">
<item quantity="other">%d วัน</item>
</plurals>
<string name="skipped_reason_not_in_release_period">ข้ามไปเนื่องจากคาดว่าจะไม่มีการออกในวันนี้</string>
<string name="manga_display_interval_title">ประมาณทุกๆ</string>
<string name="manga_display_modified_interval_title">ตั้งค่าให้อัพเดตทุกๆ</string>
<string name="pref_update_release_grace_period_info">ขอแนะนำให้ใช้ระยะเวลาเลื่อนเพื่อเพิ่มความรวดเร็วจากแหล่งที่มา ยิ่งมีการตรวจสอบรายการที่ขาดหายไปมากเท่าใด ช่วงเวลาระหว่างการตรวจสอบก็จะนานขึ้นสูงสุด 28 วัน</string>
<string name="intervals_header">ช่วงเวลา</string>
<string name="manga_modify_calculated_interval_title">ปรับแต่งช่วงเวลา</string>
<string name="action_ok">ตกลง</string>
@ -804,4 +796,6 @@
<string name="has_results">มีผลลัพธ์</string>
<string name="syncing_library">กำลังซิงค์คลัง</string>
<string name="delete_downloaded">ลบการดาวน์โหลด</string>
<string name="download_cache_invalidated">ดัชนีการดาวน์โหลดไม่ถูกต้อง</string>
<string name="information_cloudflare_help">แตะที่นี่เพื่อขอความช่วยเหลือเกี่ยวกับ Cloudflare</string>
</resources>

View File

@ -787,10 +787,6 @@
<string name="pref_chapter_swipe_start">Sola kaydırma eylemi</string>
<string name="pref_double_tap_zoom">Yakınlaştırmak için iki kez dokun</string>
<string name="pref_library_columns_per_row">Satır başına %d</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d gün önce</item>
<item quantity="other">%d gün önce</item>
</plurals>
<string name="action_set_interval">Aralığı ayarlama</string>
<string name="action_filter_interval_custom">Özelleştirilmiş fetch aralığı</string>
<string name="action_filter_interval_long">Aylık getir (28 gün)</string>
@ -804,16 +800,10 @@
<item quantity="one">1 gün</item>
<item quantity="other">%d gün</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d gün sonra</item>
<item quantity="other">%d gün sonra</item>
</plurals>
<string name="pref_update_release_grace_period">Beklenen yayınlama dönemi</string>
<string name="action_ok">TAMAM</string>
<string name="manga_display_interval_title">Hepsini tahmin et</string>
<string name="track_delete_title">%s izlemesi kaldırılsın mı\?</string>
<string name="manga_modify_calculated_interval_title">Aralığı özelleştir</string>
<string name="pref_update_release_grace_period_info">Kaynaklar üzerindeki baskıyı en aza indirgemek için düşük dönem önerilir. Bir girdinin denetimi ne kadar fazla kaçırılırsa, en fazla 28 gün olacak şekilde denetim sürelerinin aralığı o kadar uzar.</string>
<string name="skipped_reason_not_in_release_period">Atlandı çünkü bugün bir yayın beklenmiyordu</string>
<string name="track_delete_text">Bu, izlemeyi yerel olarak kaldıracak.</string>
<string name="track_delete_remote_text">Ayrıca şuradan da kaldır: %s</string>

View File

@ -821,13 +821,6 @@
<string name="action_filter_interval_dropped">Покинуто\? Останні 20+ і 2 місяці</string>
<string name="action_filter_interval_passed">Пройдено період перевірки</string>
<string name="manga_display_interval_title">Оцініть кожну</string>
<string name="pref_update_release_grace_period">Очікуваний пільговий період випуску</string>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d день після</item>
<item quantity="few">%d дні після</item>
<item quantity="many">%d днів після</item>
<item quantity="other">%d днів після</item>
</plurals>
<plurals name="day">
<item quantity="one">1 день</item>
<item quantity="few">%d дні</item>
@ -839,13 +832,6 @@
<string name="pref_update_only_in_release_period">Поза очікуваним періодом випуску</string>
<string name="action_sort_next_updated">Наступне очікуване оновлення</string>
<string name="intervals_header">Інтервали</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d день тому</item>
<item quantity="few">%d дні тому</item>
<item quantity="many">%d днів тому</item>
<item quantity="other">%d днів тому</item>
</plurals>
<string name="pref_update_release_grace_period_info">Для мінімізації навантаження на джерела рекомендується невеликий пільговий період. Чим більше перевірок запису буде пропущено, тим довшим буде інтервал між перевірками - максимум 28 днів.</string>
<string name="manga_display_modified_interval_title">Налаштовано на оновлення кожної</string>
<string name="manga_modify_calculated_interval_title">Налаштувати інтервал</string>
<string name="skipped_reason_not_in_release_period">Пропущено, оскільки сьогодні не очікується жодного релізу</string>

View File

@ -772,15 +772,7 @@
<string name="pref_chapter_swipe_start">向左滑动操作</string>
<string name="pref_double_tap_zoom">双击放大</string>
<string name="pref_library_columns_per_row">每行 %d 个</string>
<string name="pref_update_release_grace_period">预计更新时间宽限</string>
<string name="pref_update_only_in_release_period">未到预计更新时间</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">提前 %d 天</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="other">延后 %d 天</item>
</plurals>
<string name="pref_update_release_grace_period_info">建议设置较短的宽限期,这样可以减轻图源的负担。每当检查作品更新发现没有更新时,下次检查的间隔就会变长,但不会超过 28 天。</string>
<string name="action_ok">确定</string>
<string name="track_delete_title">要删除 %s 的记录吗?</string>
<string name="track_delete_remote_text">同时删除 %s 上的数据</string>
@ -805,4 +797,6 @@
<string name="syncing_library">正在同步书架</string>
<string name="library_sync_complete">书架同步完成</string>
<string name="information_cloudflare_help">点击这里查看 Cloudflare 帮助</string>
<string name="download_cache_invalidated">已清除下载索引</string>
<string name="track_activity_name">登录进度记录平台</string>
</resources>

View File

@ -3,7 +3,7 @@
<string name="categories">類別</string>
<string name="manga">藏書</string>
<string name="chapters">章節</string>
<string name="history">歷史記錄</string>
<string name="history">記錄</string>
<string name="label_settings">設定</string>
<string name="label_download_queue">下載佇列</string>
<string name="label_library">書櫃</string>
@ -74,8 +74,8 @@
<string name="update_never">關閉</string>
<string name="update_6hour">每 6 小時</string>
<string name="update_12hour">每 12 小時</string>
<string name="update_24hour"></string>
<string name="update_48hour">每 2 </string>
<string name="update_24hour"></string>
<string name="update_48hour">每 2 </string>
<string name="update_weekly">每週</string>
<string name="all">全部</string>
<string name="pref_library_update_restriction">自動更新的裝置限制</string>
@ -125,7 +125,7 @@
<string name="restoring_backup">正在還原</string>
<string name="creating_backup">正在建立備份</string>
<string name="pref_clear_chapter_cache">清除章節快取</string>
<string name="pref_clear_cookies">清除 Cookies</string>
<string name="pref_clear_cookies">清除 Cookie</string>
<string name="pref_clear_database">清除資料庫</string>
<string name="version">版本</string>
<string name="pref_enable_acra">傳送錯誤報告</string>
@ -194,7 +194,7 @@
<string name="used_cache">已使用:%1$s</string>
<string name="cache_deleted">已清除快取,%1$d 個檔案已被刪除</string>
<string name="cache_delete_error">清除時發生錯誤</string>
<string name="cookies_cleared">已清除 Cookies</string>
<string name="cookies_cleared">已清除 Cookie</string>
<string name="pref_acra_summary">協助我們修復錯誤,傳送的資料將不包含個人敏感訊息</string>
<string name="login_title">登入 %1$s</string>
<string name="username">使用者名稱</string>
@ -319,7 +319,7 @@
<string name="lock_when_idle">閒置時鎖定</string>
<string name="unlock_app">解除鎖定 Tachiyomi</string>
<string name="lock_with_biometrics">上鎖應用程式</string>
<string name="secure_screen_summary">在切換應用程式時隱藏預覽,並禁止擷取螢幕畫面</string>
<string name="secure_screen_summary">在切換應用程式時隱藏預覽,並禁止擷取螢幕畫面</string>
<string name="secure_screen">防窺畫面</string>
<string name="pref_category_security">隱私</string>
<string name="hide_notification_content">隱藏通知內容</string>
@ -601,7 +601,7 @@
<string name="backup_info">你應該在多處保存備份副本。</string>
<string name="pref_verbose_logging_summary">傾印詳細記錄至系統日誌 (將降低應用程式效能)</string>
<string name="connected_to_wifi">僅透過 Wi-Fi</string>
<string name="update_72hour">每 3 </string>
<string name="update_72hour">每 3 </string>
<string name="download_queue_size_warning">警告:大量批次下載可能壅塞來源並 (或) 使其封鎖 Tachiyomi。輕觸以瞭解詳情。</string>
<string name="ext_update_all">全部更新</string>
<string name="channel_app_updates">應用程式更新</string>
@ -784,25 +784,19 @@
<string name="action_filter_interval_dropped">放棄?延遲 20+ 和 2 個月</string>
<string name="action_filter_interval_passed">已過檢查期</string>
<string name="action_sort_next_updated">下次預期更新</string>
<string name="intervals_header">間隔</string>
<string name="pref_update_release_grace_period">預期發布寬限期</string>
<string name="intervals_header">刊期</string>
<string name="manga_display_interval_title">預計每個</string>
<string name="pref_update_release_grace_period_info">建議使用較短的寬限期以減少資源的壓力。錯過越多條目的檢查檢查之間的間隔時間就會變長最長為28天。</string>
<string name="pref_update_only_in_release_period">超出預期發行期間</string>
<plurals name="pref_update_release_leading_days">
<item quantity="other">%d 天前</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="other">%d 天後</item>
</plurals>
<plurals name="day">
<item quantity="other">%d 天</item>
</plurals>
<string name="manga_display_modified_interval_title">設定為每個</string>
<string name="manga_modify_calculated_interval_title">自訂間隔</string>
<string name="skipped_reason_not_in_release_period">跳過,因為今天不預期有新章節發佈</string>
<string name="manga_modify_calculated_interval_title">自訂刊期</string>
<string name="skipped_reason_not_in_release_period">由於非為預期出刊日,因此略過</string>
<string name="has_results">有結果</string>
<string name="syncing_library">正在同步書櫃</string>
<string name="library_sync_complete">書櫃同步完</string>
<string name="library_sync_complete">書櫃同步完</string>
<string name="download_cache_invalidated">已清除下載索引</string>
<string name="information_cloudflare_help">查看 Cloudflare 相關說明</string>
<string name="track_activity_name">登入歷程平台</string>
</resources>

View File

@ -319,6 +319,7 @@
<string name="ext_installer_legacy">Legacy</string>
<string name="ext_installer_packageinstaller" translatable="false">PackageInstaller</string>
<string name="ext_installer_shizuku" translatable="false">Shizuku</string>
<string name="ext_installer_private" translatable="false">Private</string>
<string name="ext_installer_shizuku_stopped">Shizuku is not running</string>
<string name="ext_installer_shizuku_unavailable_dialog">Install and start Shizuku to use Shizuku as extension installer.</string>
@ -467,6 +468,7 @@
<string name="enhanced_services_not_installed">Available but source not installed: %s</string>
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
<string name="action_track">Track</string>
<string name="track_activity_name">Tracking login</string>
<!-- Browse section -->
<string name="pref_hide_in_library_items">Hide entries already in library</string>
@ -499,6 +501,7 @@
<string name="creating_backup_error">Backup failed</string>
<string name="missing_storage_permission">Storage permissions not granted</string>
<string name="empty_backup_error">No library entries to back up</string>
<string name="create_backup_file_error">Couldn\'t create a backup file</string>
<string name="restore_miui_warning">Backup/restore may not function properly if MIUI Optimization is disabled.</string>
<string name="restore_in_progress">Restore is already in progress</string>
<string name="restoring_backup">Restoring backup</string>
@ -645,6 +648,7 @@
<string name="no_results_found">No results found</string>
<!-- Do not translate "WebView" -->
<string name="http_error_hint">Check website in WebView</string>
<string name="licensed_manga_chapters_error">Licensed - No chapters to show</string>
<string name="local_source">Local source</string>
<string name="other_source">Other</string>
<string name="last_used_source">Last used</string>

View File

@ -231,7 +231,6 @@ fun SelectItem(
label = { Text(text = label) },
value = options[selectedIndex].toString(),
onValueChange = {},
enabled = false,
readOnly = true,
singleLine = true,
trailingIcon = {
@ -239,9 +238,7 @@ fun SelectItem(
expanded = expanded,
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
),
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(

View File

@ -43,15 +43,16 @@ abstract class HttpSource : CatalogueSource {
open val versionId = 1
/**
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string: sourcename/language/versionId
* Note the generated id sets the sign bit to 0.
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`.
*
* The ID is generated by the [generateId] function, which can be reused if needed
* to generate outdated IDs for cases where the source name or language needs to
* be changed but migrations can be avoided.
*
* Note: the generated ID sets the sign bit to `0`.
*/
override val id by lazy {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
override val id by lazy { generateId(name, lang, versionId) }
/**
* Headers used for requests.
@ -64,6 +65,28 @@ abstract class HttpSource : CatalogueSource {
open val client: OkHttpClient
get() = network.client
/**
* Generates a unique ID for the source based on the provided [name], [lang] and
* [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string
* `"${name.lowercase()}/$lang/$versionId"`.
*
* Note: the generated ID sets the sign bit to `0`.
*
* Can be used to generate outdated IDs, such as when the source name or language
* needs to be changed but migrations can be avoided.
*
* @since extensions-lib 1.5
* @param name [String] the name of the source
* @param lang [String] the language of the source
* @param versionId [Int] the version ID of the source
* @return a unique ID for the source
*/
protected fun generateId(name: String, lang: String, versionId: Int): Long {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
@ -215,7 +238,7 @@ abstract class HttpSource : CatalogueSource {
chapterListParse(response)
}
} else {
Observable.error(Exception("Licensed - No chapters to show"))
Observable.error(LicensedMangaChaptersException())
}
}
@ -404,3 +427,5 @@ abstract class HttpSource : CatalogueSource {
*/
override fun getFilterList() = FilterList()
}
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")