Compare commits

...

17 Commits
v9.0 ... v9.0.1

Author SHA1 Message Date
Koitharu
ec048c70f1 Update parsers 2025-07-10 21:39:46 +03:00
Dragibus Noir
282c1b51f7 Translated using Weblate (French)
Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Dragibus Noir <big.confetti700@aleeas.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Infy's Tagalog Translations
d6b6ce1bcd Translated using Weblate (Filipino)
Currently translated at 99.4% (838 of 843 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Giovanni S.C
f48444dcf6 Translated using Weblate (Spanish)
Currently translated at 92.6% (781 of 843 strings)

Translated using Weblate (Spanish)

Currently translated at 92.6% (781 of 843 strings)

Co-authored-by: Giovanni S.C <giovanniandre2003@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Krays The Poet
15ba766643 Translated using Weblate (German)
Currently translated at 76.5% (645 of 843 strings)

Translated using Weblate (German)

Currently translated at 76.5% (645 of 843 strings)

Co-authored-by: Krays The Poet <kraysthepoet@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Draken
a0dbbcb350 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (845 of 845 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (843 of 843 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Frosted
f72bba9557 Translated using Weblate (Turkish)
Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Макар Разин
207791aa3e Translated using Weblate (Ukrainian)
Currently translated at 99.6% (840 of 843 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.6% (840 of 843 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (843 of 843 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (843 of 843 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2025-07-10 21:33:32 +03:00
Koitharu
6319997716 Periodical backup improvements 2025-07-10 09:54:45 +03:00
Koitharu
b70c1da54b Implement missing getForegroundInfo for LocalStorageCleanupWorker 2025-07-10 08:57:11 +03:00
Koitharu
621cb19c5b Fix saving override for non-library manga 2025-07-10 08:48:10 +03:00
Koitharu
b528b7b3c1 Fix passing headers to favicon requests 2025-07-09 22:07:37 +03:00
Koitharu
9a1bb6f6fc Fix long tap on old Android versions (close #1478) 2025-07-09 21:57:37 +03:00
Koitharu
37f9c4b9f6 Fix window insets and search closing 2025-07-09 21:42:28 +03:00
Koitharu
d0084e50e7 Fix loading local manga (closes #1481, #1474, #1479, #1484, #1439) 2025-07-09 21:21:02 +03:00
Koitharu
088576cc9d Ignore network error for background progress update 2025-07-07 13:46:37 +03:00
Koitharu
f0ba42b518 Fix captcha notification dismissing 2025-07-06 12:09:30 +03:00
47 changed files with 554 additions and 130 deletions

View File

@@ -19,8 +19,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = 1022
versionName = '9.0'
versionCode = 1023
versionName = '9.0.1'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {

View File

@@ -404,6 +404,13 @@
tools:node="remove" />
</provider>
<receiver
android:name="org.koitharu.kotatsu.core.exceptions.resolve.CaptchaHandler$DiscardReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.koitharu.kotatsu.CAPTCHA_DISCARD" />
</intent-filter>
</receiver>
<receiver
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider"
android:exported="true"

View File

@@ -30,21 +30,19 @@ constructor(
oldManga: Manga,
newManga: Manga,
) {
val oldDetails =
if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails =
if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails)
val oldDetails = if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails = if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails, replaceExisting = true)
database.withTransaction {
// replace favorites
val favoritesDao = database.getFavouritesDao()
@@ -101,11 +99,11 @@ constructor(
mangaId = newDetails.id,
rating = prevInfo.rating,
status =
prevInfo.status ?: when {
newHistory == null -> ScrobblingStatus.PLANNED
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
prevInfo.status ?: when {
newHistory == null -> ScrobblingStatus.PLANNED
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
comment = prevInfo.comment,
)
if (newHistory != null) {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.backups.ui
import android.content.Context
import android.net.Uri
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
@@ -27,24 +28,13 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(applicationContext)
createNotificationChannel()
createNotificationChannel(this)
}
override fun IntentJobContext.onError(error: Throwable) {
showResultNotification(null, CompositeResult.failure(error))
}
private fun createNotificationChannel() {
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.backup_restore))
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(false)
.build()
notificationManager.createNotificationChannel(channel)
}
protected fun IntentJobContext.showResultNotification(
fileUri: Uri?,
result: CompositeResult,
@@ -128,8 +118,19 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() {
.setBigContentTitle(title),
)
protected companion object {
companion object {
const val CHANNEL_ID = "backup_restore"
fun createNotificationChannel(context: Context) {
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(context.getString(R.string.backup_restore))
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(false)
.build()
NotificationManagerCompat.from(context).createNotificationChannel(channel)
}
}
}

View File

@@ -1,12 +1,21 @@
package org.koitharu.kotatsu.backups.ui.periodical
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.backups.domain.BackupUtils
import org.koitharu.kotatsu.backups.domain.ExternalBackupStorage
import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import java.util.zip.ZipOutputStream
import javax.inject.Inject
@@ -48,5 +57,49 @@ class PeriodicalBackupService : CoroutineIntentService() {
}
}
override fun IntentJobContext.onError(error: Throwable) = Unit
override fun IntentJobContext.onError(error: Throwable) {
if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) {
return
}
BaseBackupRestoreService.createNotificationChannel(applicationContext)
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(0)
.setSilent(true)
.setAutoCancel(true)
val title = getString(R.string.periodic_backups)
val message = getString(
R.string.inline_preference_pattern,
getString(R.string.packup_creation_failed),
error.getDisplayMessage(resources),
)
notification
.setContentText(message)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(message)
.setSummaryText(getString(R.string.packup_creation_failed))
.setBigContentTitle(title),
)
ErrorReporterReceiver.getNotificationAction(applicationContext, error, startId, TAG)?.let { action ->
notification.addAction(action)
}
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
0,
AppRouter.periodicBackupSettingsIntent(applicationContext),
0,
false,
),
)
NotificationManagerCompat.from(applicationContext).notify(TAG, startId, notification.build())
}
private companion object {
const val CHANNEL_ID = BaseBackupRestoreService.CHANNEL_ID
const val TAG = "periodical_backup"
}
}

View File

@@ -6,6 +6,7 @@ import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import androidx.activity.result.ActivityResultCallback
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.preference.EditTextPreference
import androidx.preference.Preference
@@ -84,6 +85,13 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
"" -> null
else -> path
}
preference.icon = if (path == null) {
ContextCompat.getDrawable(preference.context, R.drawable.ic_alert_outline)?.also {
it.setTint(ContextCompat.getColor(preference.context, R.color.warning))
}
} else {
null
}
}
private fun bindLastBackupInfo(lastBackupDate: Date?) {

View File

@@ -5,7 +5,6 @@ import android.app.Notification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresPermission
@@ -14,7 +13,6 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.lifecycle.coroutineScope
import coil3.EventListener
@@ -26,6 +24,7 @@ import coil3.request.allowConversionToBitmap
import coil3.request.allowHardware
import coil3.request.lifecycle
import coil3.size.Scale
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@@ -70,22 +69,6 @@ class CaptchaHandler @Inject constructor(
private val exceptionMap = MutableScatterMap<MangaSource, CloudFlareProtectedException>()
private val mutex = Mutex()
init {
ContextCompat.registerReceiver(
context,
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val sourceName = intent?.getStringExtra(AppRouter.KEY_SOURCE) ?: return
goAsync {
handleException(MangaSource(sourceName), exception = null, notify = false)
}
}
},
IntentFilter().apply { addAction(ACTION_DISCARD) },
ContextCompat.RECEIVER_NOT_EXPORTED,
)
}
suspend fun handle(exception: CloudFlareException): Boolean = handleException(exception.source, exception, true)
suspend fun discard(source: MangaSource) {
@@ -251,6 +234,20 @@ class CaptchaHandler @Inject constructor(
it.printStackTraceDebug()
}.getOrNull()
@AndroidEntryPoint
class DiscardReceiver : BroadcastReceiver() {
@Inject
lateinit var captchaHandler: CaptchaHandler
override fun onReceive(context: Context?, intent: Intent?) {
val sourceName = intent?.getStringExtra(AppRouter.KEY_SOURCE) ?: return
goAsync {
captchaHandler.handleException(MangaSource(sourceName), exception = null, notify = false)
}
}
}
companion object {
fun ImageRequest.Builder.ignoreCaptchaErrors() = apply {

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.image
import coil3.intercept.Interceptor
import coil3.network.httpHeaders
import coil3.request.ImageResult
import org.koitharu.kotatsu.core.model.unwrap
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.util.ext.mangaSourceKey
import org.koitharu.kotatsu.parsers.model.MangaParserSource
@@ -10,7 +11,7 @@ import org.koitharu.kotatsu.parsers.model.MangaParserSource
class MangaSourceHeaderInterceptor : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val mangaSource = chain.request.extras[mangaSourceKey] as? MangaParserSource ?: return chain.proceed()
val mangaSource = chain.request.extras[mangaSourceKey]?.unwrap() as? MangaParserSource ?: return chain.proceed()
val request = chain.request
val newHeaders = request.httpHeaders.newBuilder()
.set(CommonHeaders.MANGA_SOURCE, mangaSource.name)

View File

@@ -741,6 +741,10 @@ class AppRouter private constructor(
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_TRACKER)
fun periodicBackupSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_PERIODIC_BACKUP)
fun proxySettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_PROXY)
@@ -825,6 +829,7 @@ class AppRouter private constructor(
const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
const val ACTION_PERIODIC_BACKUP = "${BuildConfig.APPLICATION_ID}.action.MANAGE_PERIODIC_BACKUP"
private const val ACCOUNT_KEY = "account"
private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS"

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.network
import android.util.Log
import dagger.Lazy
import okhttp3.Headers
import okhttp3.Interceptor
@@ -36,7 +35,8 @@ class CommonHeadersInterceptor @Inject constructor(
mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
} else {
if (BuildConfig.DEBUG && source == null) {
Log.w("Http", "Request without source tag: ${request.url}")
IllegalArgumentException("Request without source tag: ${request.url}")
.printStackTrace()
}
null
}

View File

@@ -149,7 +149,7 @@ class AppShortcutManager @Inject constructor(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
mangaRepository.storeManga(manga)
mangaRepository.storeManga(manga, replaceExisting = true)
val title = manga.title.ifEmpty {
manga.altTitles.firstOrNull()
}.ifNullOrEmpty {

View File

@@ -13,7 +13,6 @@ import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityOptionsCompat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
// https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr
class OpenDocumentTreeHelper(
@@ -28,38 +27,42 @@ class OpenDocumentTreeHelper(
callback,
)
private val pickFileTreeLauncherQ = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activityResultCaller.registerForActivityResult(OpenDocumentTreeContractQ(flags), callback)
private val pickFileTreeLauncherPrimaryStorage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activityResultCaller.registerForActivityResult(OpenDocumentTreeContractPrimaryStorage(flags), callback)
} else {
null
}
private val pickFileTreeLauncherLegacy = activityResultCaller.registerForActivityResult(
contract = OpenDocumentTreeContractLegacy(flags),
private val pickFileTreeLauncherDefault = activityResultCaller.registerForActivityResult(
contract = OpenDocumentTreeContractDefault(flags),
callback = callback,
)
override fun launch(input: Uri?, options: ActivityOptionsCompat?) {
if (pickFileTreeLauncherQ == null) {
pickFileTreeLauncherLegacy.launch(input, options)
return
}
try {
pickFileTreeLauncherQ.launch(input, options)
pickFileTreeLauncherDefault.launch(input, options)
} catch (e: Exception) {
e.printStackTraceDebug()
pickFileTreeLauncherLegacy.launch(input, options)
if (pickFileTreeLauncherPrimaryStorage != null) {
try {
pickFileTreeLauncherPrimaryStorage.launch(input, options)
} catch (e2: Exception) {
e.addSuppressed(e2)
throw e
}
} else {
throw e
}
}
}
override fun unregister() {
pickFileTreeLauncherQ?.unregister()
pickFileTreeLauncherLegacy.unregister()
pickFileTreeLauncherPrimaryStorage?.unregister()
pickFileTreeLauncherDefault.unregister()
}
override val contract: ActivityResultContract<Uri?, *>
get() = pickFileTreeLauncherQ?.contract ?: pickFileTreeLauncherLegacy.contract
get() = pickFileTreeLauncherPrimaryStorage?.contract ?: pickFileTreeLauncherDefault.contract
private open class OpenDocumentTreeContractLegacy(
private open class OpenDocumentTreeContractDefault(
private val flags: Int,
) : ActivityResultContracts.OpenDocumentTree() {
@@ -71,9 +74,9 @@ class OpenDocumentTreeHelper(
}
@RequiresApi(Build.VERSION_CODES.Q)
private class OpenDocumentTreeContractQ(
private class OpenDocumentTreeContractPrimaryStorage(
private val flags: Int,
) : OpenDocumentTreeContractLegacy(flags) {
) : OpenDocumentTreeContractDefault(flags) {
override fun createIntent(context: Context, input: Uri?): Intent {
val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager)

View File

@@ -41,7 +41,7 @@ class MangaDataRepository @Inject constructor(
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
db.withTransaction {
storeManga(manga)
storeManga(manga, replaceExisting = false)
val entity = db.getPreferencesDao().find(manga.id) ?: newEntity(manga.id)
db.getPreferencesDao().upsert(entity.copy(mode = mode.id))
}
@@ -49,7 +49,7 @@ class MangaDataRepository @Inject constructor(
suspend fun saveColorFilter(manga: Manga, colorFilter: ReaderColorFilter?) {
db.withTransaction {
storeManga(manga)
storeManga(manga, replaceExisting = false)
val entity = db.getPreferencesDao().find(manga.id) ?: newEntity(manga.id)
db.getPreferencesDao().upsert(
entity.copy(
@@ -87,10 +87,11 @@ class MangaDataRepository @Inject constructor(
return map
}
suspend fun setOverride(mangaId: Long, override: MangaOverride?) {
suspend fun setOverride(manga: Manga, override: MangaOverride?) {
db.withTransaction {
storeManga(manga, replaceExisting = false)
val dao = db.getPreferencesDao()
val entity = dao.find(mangaId) ?: newEntity(mangaId)
val entity = dao.find(manga.id) ?: newEntity(manga.id)
dao.upsert(
entity.copy(
titleOverride = override?.title?.nullIfEmpty(),
@@ -127,7 +128,10 @@ class MangaDataRepository @Inject constructor(
else -> null
}
suspend fun storeManga(manga: Manga) {
suspend fun storeManga(manga: Manga, replaceExisting: Boolean) {
if (!replaceExisting && db.getMangaDao().find(manga.id) != null) {
return
}
db.withTransaction {
// avoid storing local manga if remote one is already stored
val existing = if (manga.isLocal) {
@@ -185,7 +189,7 @@ class MangaDataRepository @Inject constructor(
emitInitialState = emitInitialState,
)
private suspend fun Manga.withCachedChaptersIfNeeded(flag: Boolean): Manga = if (flag && chapters.isNullOrEmpty()) {
private suspend fun Manga.withCachedChaptersIfNeeded(flag: Boolean): Manga = if (flag && !isLocal && chapters.isNullOrEmpty()) {
val cachedChapters = db.getChaptersDao().findAll(id)
if (cachedChapters.isEmpty()) {
this

View File

@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.util.ext.EventFlow
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -80,10 +81,15 @@ abstract class BaseViewModel : ViewModel() {
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
private fun createErrorHandler() = CoroutineExceptionHandler { coroutineContext, throwable ->
throwable.printStackTraceDebug()
if (throwable !is CancellationException) {
if (coroutineContext[SkipErrors.key] == null && throwable !is CancellationException) {
errorEvent.call(throwable)
}
}
protected object SkipErrors : AbstractCoroutineContextElement(Key) {
private object Key : CoroutineContext.Key<SkipErrors>
}
}

View File

@@ -175,6 +175,14 @@ fun View.setOnContextClickListenerCompat(listener: OnContextClickListenerCompat)
}
}
fun View.setTooltipCompat(tooltip: CharSequence?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
tooltipText = tooltip
} else if (!isLongClickable) { // don't use TooltipCompat if has a LongClickListener
TooltipCompat.setTooltipText(this, tooltip)
}
}
val Toolbar.menuView: ActionMenuView?
get() {
menu // to call ensureMenu()
@@ -201,7 +209,7 @@ fun Chip.setProgressIcon() {
fun View.setContentDescriptionAndTooltip(@StringRes resId: Int) {
val text = resources.getString(resId)
contentDescription = text
TooltipCompat.setTooltipText(this, text)
setTooltipCompat(text)
}
fun View.getWindowBounds(): Rect {

View File

@@ -58,17 +58,26 @@ class DetailsLoadUseCase @Inject constructor(
isLoaded = false,
),
)
val local = if (!manga.isLocal) {
async {
localMangaRepository.findSavedManga(manga)
}
} else {
null
if (manga.isLocal) {
val details = getDetails(manga, force)
send(
MangaDetails(
manga = details,
localManga = null,
override = override,
description = details.description?.parseAsHtml(withImages = false)?.trim(),
isLoaded = true,
),
)
return@channelFlow
}
val local = async {
localMangaRepository.findSavedManga(manga)
}
if (!force && networkState.isOfflineOrRestricted()) {
// try to avoid loading if has saved manga
val localManga = local?.await()
if (manga.isLocal || localManga != null) {
val localManga = local.await()
if (localManga != null) {
send(
MangaDetails(
manga = manga,
@@ -88,7 +97,7 @@ class DetailsLoadUseCase @Inject constructor(
send(
MangaDetails(
manga = details,
localManga = local?.peek(),
localManga = local.peek(),
override = override,
description = details.description?.parseAsHtml(withImages = false)?.trim(),
isLoaded = false,
@@ -97,14 +106,14 @@ class DetailsLoadUseCase @Inject constructor(
send(
MangaDetails(
manga = details,
localManga = local?.await(),
localManga = local.await(),
override = override,
description = details.description?.parseAsHtml(withImages = true)?.trim(),
isLoaded = true,
),
)
} catch (e: IOException) {
local?.await()?.manga?.also { localManga ->
local.await()?.manga?.also { localManga ->
send(
MangaDetails(
manga = localManga,

View File

@@ -80,6 +80,7 @@ import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.start
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
@@ -455,7 +456,7 @@ class DetailsActivity :
textViewSourceLabel.isVisible = false
} else {
textViewSource.textAndVisible = manga.source.getTitle(this@DetailsActivity)
TooltipCompat.setTooltipText(textViewSource, manga.source.getSummary(this@DetailsActivity))
textViewSource.setTooltipCompat(manga.source.getSummary(this@DetailsActivity))
textViewSourceLabel.isVisible = textViewSource.isVisible == true
}
val faviconPlaceholderFactory = FaviconDrawable.Factory(R.style.FaviconDrawable_Chip)

View File

@@ -182,7 +182,7 @@ class DetailsViewModel @Inject constructor(
init {
loadingJob = doLoad(force = false)
launchJob(Dispatchers.Default) {
launchJob(Dispatchers.Default + SkipErrors) {
val manga = mangaDetails.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
val h = history.firstOrNull()
if (h != null) {

View File

@@ -1,12 +1,12 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Typeface
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.databinding.ItemChapterGridBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -23,7 +23,7 @@ fun chapterGridItemAD(
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.numberString() ?: "?"
TooltipCompat.setTooltipText(itemView, item.chapter.title)
itemView.setTooltipCompat(item.chapter.title)
}
binding.imageViewNew.isVisible = item.isNew
binding.imageViewCurrent.isVisible = item.isCurrent

View File

@@ -537,7 +537,7 @@ class DownloadWorker @AssistedInject constructor(
return
}
val requests = tasks.map { (manga, task) ->
mangaDataRepository.storeManga(manga)
mangaDataRepository.storeManga(manga, replaceExisting = true)
OneTimeWorkRequestBuilder<DownloadWorker>()
.setConstraints(createConstraints(task.allowMeteredNetwork))
.addTag(TAG)

View File

@@ -24,7 +24,7 @@ class RecoverMangaUseCase @Inject constructor(
repository.getDetails(it)
} ?: return@runCatchingCancellable null
val merged = merge(manga, newManga)
mangaDataRepository.storeManga(merged)
mangaDataRepository.storeManga(merged, replaceExisting = true)
merged
}.onFailure {
it.printStackTraceDebug()

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.setProgressIcon
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceGridBinding
@@ -126,8 +127,7 @@ fun exploreSourceGridItemAD(
bind {
val title = item.source.getTitle(context)
TooltipCompat.setTooltipText(
itemView,
itemView.setTooltipCompat(
buildSpannedString {
bold {
append(title)

View File

@@ -118,7 +118,7 @@ class HistoryRepository @Inject constructor(
}
assert(manga.chapters != null)
db.withTransaction {
mangaRepository.storeManga(manga)
mangaRepository.storeManga(manga, replaceExisting = true)
val branch = manga.chapters?.findById(chapterId)?.branch
db.getHistoryDao().upsert(
HistoryEntity(

View File

@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -24,7 +24,7 @@ fun mangaGridItemAD(
sizeResolver.attachToView(itemView, binding.textViewTitle, binding.progressView)
bind { payloads ->
TooltipCompat.setTooltipText(itemView, item.getSummary(context))
itemView.setTooltipCompat(item.getSummary(context))
binding.textViewTitle.text = item.title
binding.progressView.setProgress(item.progress, PAYLOAD_PROGRESS_CHANGED in payloads)
with(binding.iconsView) {

View File

@@ -5,6 +5,7 @@ import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -20,7 +21,7 @@ fun mangaListItemAD(
AdapterDelegateClickListenerAdapter(this, clickListener, MangaCompactListModel::manga).attach(itemView)
bind {
TooltipCompat.setTooltipText(itemView, item.getSummary(context))
itemView.setTooltipCompat(item.getSummary(context))
binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.imageViewCover.setImageAsync(item.coverUrl, item.manga)

View File

@@ -97,7 +97,7 @@ class LocalMangaIndex @Inject constructor(
}
private suspend fun upsert(manga: LocalManga) {
mangaDataRepository.storeManga(manga.manga)
mangaDataRepository.storeManga(manga.manga, replaceExisting = true)
db.getLocalMangaIndexDao().upsert(manga.toEntity())
}

View File

@@ -1,10 +1,19 @@
package org.koitharu.kotatsu.local.ui
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.provider.Settings
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.hilt.work.HiltWorker
import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
@@ -12,6 +21,8 @@ import androidx.work.WorkerParameters
import androidx.work.await
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.LocalMangaRepository
@@ -40,9 +51,63 @@ class LocalStorageCleanupWorker @AssistedInject constructor(
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
val title = applicationContext.getString(R.string.local_storage_cleanup)
val channel = NotificationChannelCompat.Builder(WORKER_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(title)
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(true)
.build()
NotificationManagerCompat.from(applicationContext).createNotificationChannel(channel)
val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID)
.setContentTitle(title)
.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,
0,
AppRouter.suggestionsSettingsIntent(applicationContext),
0,
false,
),
)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setDefaults(0)
.setOngoing(false)
.setSilent(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val actionIntent = PendingIntentCompat.getActivity(
applicationContext, SETTINGS_ACTION_CODE,
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, applicationContext.packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, WORKER_CHANNEL_ID),
0, false,
)
notification.addAction(
R.drawable.ic_settings,
applicationContext.getString(R.string.notifications_settings),
actionIntent,
)
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ForegroundInfo(WORKER_NOTIFICATION_ID, notification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
ForegroundInfo(WORKER_NOTIFICATION_ID, notification.build())
}
}
companion object {
private const val TAG = "cleanup"
private const val WORKER_CHANNEL_ID = "storage_cleanup"
private const val WORKER_NOTIFICATION_ID = 32
private const val SETTINGS_ACTION_CODE = 6
suspend fun enqueue(context: Context) {
val request = OneTimeWorkRequestBuilder<LocalStorageCleanupWorker>()

View File

@@ -72,7 +72,7 @@ class CoverRestoreInterceptor @Inject constructor(
val repo = repositoryFactory.create(manga.source)
val fixed = repo.find(manga) ?: return false
return if (fixed != manga) {
dataRepository.storeManga(fixed)
dataRepository.storeManga(fixed, replaceExisting = true)
fixed.coverUrl != manga.coverUrl
} else {
false

View File

@@ -131,6 +131,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
onBackPressedDispatcher.addCallback(exitCallback)
onBackPressedDispatcher.addCallback(navigationDelegate)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
val legacySearchCallback = SearchViewLegacyBackCallback(viewBinding.searchView)
viewBinding.searchView.addTransitionListener(legacySearchCallback)
onBackPressedDispatcher.addCallback(legacySearchCallback)
}
if (savedInstanceState == null) {
onFirstStart()
}

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.main.ui
import android.os.Build
import androidx.activity.OnBackPressedCallback
import androidx.annotation.DeprecatedSinceApi
import com.google.android.material.search.SearchView
@DeprecatedSinceApi(Build.VERSION_CODES.TIRAMISU)
class SearchViewLegacyBackCallback(
private val searchView: SearchView
) : OnBackPressedCallback(searchView.isShowing), SearchView.TransitionListener {
override fun handleOnBackPressed() {
searchView.hide()
}
override fun onStateChanged(
searchView: SearchView,
previousState: SearchView.TransitionState,
newState: SearchView.TransitionState
) {
isEnabled = newState >= SearchView.TransitionState.SHOWING
}
}

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderControl
import org.koitharu.kotatsu.core.util.ext.hasVisibleChildren
import org.koitharu.kotatsu.core.util.ext.isRtl
import org.koitharu.kotatsu.core.util.ext.setContentDescriptionAndTooltip
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.databinding.LayoutReaderActionsBinding
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
@@ -254,7 +255,7 @@ class ReaderActionsView @JvmOverloads constructor(
private fun Button.initAction() {
setOnClickListener(this@ReaderActionsView)
setOnLongClickListener(this@ReaderActionsView)
TooltipCompat.setTooltipText(this, contentDescription)
setTooltipCompat(contentDescription)
}
private fun isAutoRotationEnabled(): Boolean = Settings.System.getInt(

View File

@@ -351,11 +351,11 @@ class ReaderActivity :
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
gestureInsets = insets.getInsets(WindowInsetsCompat.Type.systemGestures())
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
viewBinding.appbarTop.updatePadding(
top = systemBars.top,
right = systemBars.right,
left = systemBars.left,
)
viewBinding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemBars.top
rightMargin = systemBars.right
leftMargin = systemBars.left
}
if (viewBinding.toolbarDocked != null) {
viewBinding.actionsView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = systemBars.bottom

View File

@@ -10,6 +10,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionMangaGridBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
@@ -47,7 +48,7 @@ private fun searchSuggestionMangaGridAD(
}
bind {
TooltipCompat.setTooltipText(itemView, item.title)
itemView.setTooltipCompat(item.title)
binding.imageViewCover.setImageAsync(item.coverUrl, item.source)
binding.textViewTitle.text = item.title
}

View File

@@ -18,6 +18,7 @@ import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.BaseActivity
@@ -146,6 +147,7 @@ class SettingsActivity :
AppRouter.ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
AppRouter.ACTION_HISTORY -> UserDataSettingsFragment()
AppRouter.ACTION_TRACKER -> TrackerSettingsFragment()
AppRouter.ACTION_PERIODIC_BACKUP -> PeriodicalBackupSettingsFragment()
AppRouter.ACTION_SOURCES -> SourcesSettingsFragment()
AppRouter.ACTION_PROXY -> ProxySettingsFragment()
AppRouter.ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()

View File

@@ -37,7 +37,7 @@ class OverrideConfigViewModel @Inject constructor(
val override = checkNotNull(data.value).second.copy(
title = title,
)
dataRepository.setOverride(manga.id, override)
dataRepository.setOverride(manga, override)
onSaved.call(Unit)
}
}

View File

@@ -1,12 +1,12 @@
package org.koitharu.kotatsu.settings.storage.directories
import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding
import org.koitharu.kotatsu.settings.storage.DirectoryModel
@@ -18,7 +18,7 @@ fun directoryConfigAD(
) {
binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
TooltipCompat.setTooltipText(binding.buttonRemove, binding.buttonRemove.contentDescription)
binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription)
bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes)

View File

@@ -826,4 +826,13 @@
<string name="adblock_summary">Блакаванне рэкламы ва ўбудаваным браўзэры (бэта)</string>
<string name="changelog">Часопіс змен</string>
<string name="changelog_summary">Гісторыя змен для нядаўна выпушчаных версій</string>
<string name="reader_navigation_inverted">Інвертаваць элементы кіравання навігацыяй</string>
<string name="reader_navigation_inverted_summary">Памяняць месцамі кірунак кнопкі рэгулявання гучнасці і апаратнай клавішы навігацыі (налева/уверх/уніз/направа)</string>
<string name="creating_backup">Стварэнне рэзервовай копіі</string>
<string name="share_backup">Падзяліцца рэзервовай копіяй</string>
<string name="reader_multitask">Адчыняць чыталку як асобную задачу</string>
<string name="reader_multitask_summary">Дазваляе трымаць адкрытымі некалькі чыталак з рознай мангой адначасова</string>
<string name="theme_name_itsuka">Іцука</string>
<string name="theme_name_totoro">Тоторо</string>
<string name="book_effect">Жаўтлявы фон (фільтр сіняга)</string>
</resources>

View File

@@ -633,4 +633,22 @@
<string name="recent_queries">Kürzliche Suchen</string>
<string name="suggested_queries">Vorgeschlagene Suchen</string>
<string name="authors">Autoren</string>
<string name="nsfw_16">16+</string>
<string name="blocked_by_server_message">Du wurdest vom Server blockiert. Versuche, eine andere Netzwerkverbindung zu benutzen (VPN, Proxy, etc.)</string>
<string name="disable">Deaktivieren</string>
<string name="sources_disabled">Quellen deaktiviert</string>
<string name="disable_nsfw_notifications">Deaktiviere NSFW Benachrichtigungen</string>
<string name="disable_nsfw_notifications_summary">Zeige keine Benachrichtigungen bezüglich NSFW Manga updates</string>
<string name="_new">Neues</string>
<string name="all_languages">Alle Sprachen</string>
<string name="screenshots_block_incognito">Blockieren, wenn im Inkognito-Modus</string>
<string name="image_server">Bevorzugter Server</string>
<string name="pin">Pinnen</string>
<string name="unpin">Entpinnen</string>
<string name="source_pinned">Quelle gepinnt</string>
<string name="percent_read">Prozent gelesen</string>
<string name="percent_left">Prozent übrig</string>
<string name="chapters_read">Kapitel gelesen</string>
<string name="chapters_left">Kapitel übrig</string>
<string name="plugin_incompatible">Inkompatibles Plugin oder Interner Fehler. Stelle sicher, dass du die letzte Version von Kotatsu und die des Plugins verwendest</string>
</resources>

View File

@@ -776,4 +776,12 @@
<string name="dont_ask_again">No volver a preguntar</string>
<string name="incognito_mode_hint_nsfw">Este manga puede contener contenido adulto. ¿Quieres usar el modo incógnito?</string>
<string name="expand">Expandir</string>
<string name="enable_all_sources_summary">Todas las fuentes de manga disponibles se habilitarán permanentemente</string>
<string name="all_sources_enabled">Todas las fuentes están habilitadas</string>
<string name="backup_restored_background">La copia de seguridad se restaurará en segundo plano</string>
<string name="restoring_backup">Restaurando copia de seguridad</string>
<string name="screen_rotation_locked">La rotación de pantalla ha sido bloqueada</string>
<string name="screen_rotation_unlocked">La rotación de pantalla se ha desbloqueado</string>
<string name="simple">Simple</string>
<string name="global_search">Búsqueda global</string>
</resources>

View File

@@ -827,4 +827,8 @@
<string name="creating_backup">Ginagawa ang backup</string>
<string name="share_backup">I-share ang backup</string>
<string name="reader_navigation_inverted">Baliktarin ang mga na kontrol sa nabigasyon</string>
<string name="reader_navigation_inverted_summary">Baliktarin ang direksyon ng volume button at ang nabigasyon ng directional hardware key (kaliwa/tass/baba/kanan)</string>
<string name="reader_multitask">Buksan ang reader sa isang hiwalay na gawain</string>
<string name="reader_multitask_summary">Binibigyang-daan kang panatilihing bukas ang maraming reader na may magkakaibang manga nang sabay-sabay</string>
<string name="book_effect">Mala-dilaw na background (asul na filter)</string>
</resources>

View File

@@ -830,4 +830,7 @@
<string name="theme_name_itsuka">Itsuka</string>
<string name="theme_name_totoro">Totoro</string>
<string name="reader_multitask">Ouvrir le lecteur dans une tâche séparée</string>
<string name="reader_navigation_inverted">Inverser les commandes de navigation</string>
<string name="reader_navigation_inverted_summary">Inverser le sens des boutons de volume et des touches directionnelles matérielles (gauche/haut/bas/droite)</string>
<string name="book_effect">Fond jaunâtre (filtre bleu)</string>
</resources>

View File

@@ -331,7 +331,7 @@
<string name="clear_new_chapters_counters">Также очистить информацию о новых главах</string>
<string name="server_error">Внутренняя ошибка сервера (%1$d). Повторите попытку позже</string>
<string name="compact">Компактный</string>
<string name="source_disabled">Источник отключен</string>
<string name="source_disabled">Источник отключён</string>
<string name="prefetch_content">Предварительная загрузка содержимого</string>
<string name="mark_as_current">Пометить как текущую</string>
<string name="language">Язык</string>
@@ -492,7 +492,7 @@
<string name="manage_sources">Управление источниками</string>
<string name="no_manga_sources_found">Не найдено доступных источников по вашему запросу</string>
<string name="manual">Вручную</string>
<string name="source_enabled">Источник включен</string>
<string name="source_enabled">Источник включён</string>
<string name="disable_nsfw_summary">Отключить NSFW источники и скрывать мангу для взрослых в списках, если это возможно</string>
<string name="no_manga_sources_catalog_text">В этом разделе нет доступных источников, или все они могли быть уже добавлены.
\nСледите за обновлениями</string>
@@ -788,7 +788,7 @@
<string name="error_details">Подробности ошибки</string>
<string name="error_disclaimer_app_outdated">Похоже, что Ваша версия Kotatsu устарела. Пожалуйста, установите последнюю версию, чтобы получить все доступные исправления.</string>
<string name="error_disclaimer_report">Вы можете отправить отчёт об ошибке разработчикам. Это поможет нам исправить проблему.</string>
<string name="search_disabled_sources">Поиск по отключенным источникам</string>
<string name="search_disabled_sources">Поиск по отключённым источникам</string>
<string name="chapter_volume_number">Том %1$s Глава %2$s</string>
<string name="chapter_number">Глава %s</string>
<string name="unnamed_chapter">Безымянная глава</string>
@@ -833,4 +833,6 @@
<string name="theme_name_itsuka">Ицука</string>
<string name="theme_name_totoro">Тоторо</string>
<string name="book_effect">Желтоватый фон (фильтр синего)</string>
<string name="reader_navigation_inverted_summary">Поменять местами направление кнопки регулировки громкости и аппаратной клавиши навигации (влево/вверх/вниз/вправо)</string>
<string name="reader_multitask_summary">Позволяет держать открытыми несколько читалок с разной мангой одновременно</string>
</resources>

View File

@@ -834,4 +834,5 @@
<string name="theme_name_totoro">Totoro</string>
<string name="reader_navigation_inverted">Navigasyon kontrollerini ters çevir</string>
<string name="reader_navigation_inverted_summary">Ses tuşlarının yönlerini değiştir ve donanımsal yön tuşlarını yer değiştir (sol/yukarı/aşağı/sağ)</string>
<string name="book_effect">Sarımsı arkaplan (mavi filtre)</string>
</resources>

View File

@@ -2,7 +2,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="delete">Видалити</string>
<string name="nothing_found">Нічого не знайдено</string>
<string name="add_to_favourites">Додати до улюблених</string>
<string name="add_to_favourites">До улюблених</string>
<string name="clear_history">Очистити історію</string>
<string name="history_is_empty">Історії ще немає</string>
<string name="add">Додати</string>
@@ -30,7 +30,7 @@
<string name="read">Читати</string>
<string name="grid">Таблиця</string>
<string name="share">Поділитися</string>
<string name="create_shortcut">Створити ярлик</string>
<string name="create_shortcut">Створити ярлик</string>
<string name="share_s">Поділитися %s</string>
<string name="search">Пошук</string>
<string name="search_manga">Пошук манґи</string>
@@ -49,7 +49,7 @@
<string name="remove">Видалити</string>
<string name="_s_deleted_from_local_storage">«%s» видалено з локального сховища</string>
<string name="save_page">Зберегти сторінку</string>
<string name="page_saved">Збережено</string>
<string name="page_saved">Сторінку збережено</string>
<string name="share_image">Поділитись зображенням</string>
<string name="operation_not_supported">Ця операція не підтримується</string>
<string name="text_file_not_supported">Виберіть файл ZIP або CBZ.</string>
@@ -315,7 +315,7 @@
<string name="manga_error_description_pattern">Деталі помилки:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Спробуйте &lt;a href=%2$s&gt;відкрити манґу у веб-браузері&lt;/a&gt;, щоб переконатися, що вона доступна в джерелі&lt;br&gt;2. Переконайтеся, що ви використовуєте &lt;a href=kotatsu://about&gt;останню версію Kotatsu&lt;/a&gt;&lt;br&gt;3. Якщо він доступний, надішліть звіт про помилку розробникам.</string>
<string name="history_shortcuts">Показувати ярлики останньої прочитаної манґи</string>
<string name="history_shortcuts_summary">Зробити нещодавно прочитану манґу доступною за довгим натисканням на іконку застосунку</string>
<string name="reader_control_ltr_summary">Натискання на правий край або натискання правої клавіші завжди перемикається на наступну сторінку.</string>
<string name="reader_control_ltr_summary">Не налаштовуйте напрям перемикання сторінок у режим читання, наприклад, натискання правої клавіші завжди перемикається на наступну сторінку. Ця опція впливає лише на апаратні пристрої введення</string>
<string name="reader_control_ltr">Ергономічне керування режимом читання</string>
<string name="brightness">Яскравість</string>
<string name="color_correction">Корекція кольору</string>
@@ -384,7 +384,7 @@
<string name="suggestions_notifications_summary">Іноді показувати сповіщення з запропонованою манґою</string>
<string name="more">Більше</string>
<string name="cancel_all_downloads_confirm">Усі активні завантаження буде скасовано, частково завантажені дані буде втрачено</string>
<string name="remove_completed_downloads_confirm">Ваша історія завантажень буде остаточно видалена</string>
<string name="remove_completed_downloads_confirm">Вашу історію завантажень буде видалено назавжди. Завантажені файли не будуть порушені</string>
<string name="text_downloads_list_holder">У вас немає завантажень</string>
<string name="downloads_resumed">Завантаження відновлено</string>
<string name="downloads_paused">Завантаження призупинено</string>
@@ -661,4 +661,175 @@
<string name="text_empty_holder_secondary_filtered">Немає манґи, що відповідає вибраним вами фільтрам</string>
<string name="connection_ok">З\'єднання в порядку</string>
<string name="invalid_proxy_configuration">Неправильна конфігурація проксі</string>
<string name="retry">Повторити</string>
<string name="pages_saved">Сторінки збережені</string>
<string name="nsfw_16">16+</string>
<string name="invalid_server_address_message">Невірна адреса сервера</string>
<string name="theme_name_expressive">Виразний (Тест)</string>
<string name="too_many_requests_message_retry">Забагато запитів. Спробуйте ще раз через %s</string>
<string name="reader_navigation_inverted">Інвертувати елементи керування навігацією</string>
<string name="reader_navigation_inverted_summary">Поміняти місцями напрямок кнопки регулювання гучності та апаратної клавіші навігації (вліво/вгору/вниз/вправо)</string>
<string name="seconds_short">%d с</string>
<string name="minutes_seconds_short">%1$d хв %2$d с</string>
<string name="plugin_incompatible_with_cause">Помилка плагіна: %s\nПереконайтеся, що ви використовуєте останню версію плагіна та Kotatsu</string>
<string name="show_quick_filters">Показати швидкі фільтри</string>
<string name="show_quick_filters_summary">Надає можливість фільтрувати списки манги за певними параметрами</string>
<string name="sfw">SFW</string>
<string name="skip_all">Пропустити все</string>
<string name="stuck">Зависло</string>
<string name="not_in_favorites">Немає в обраних</string>
<string name="updated_long_ago">Оновлено давно</string>
<string name="unpopular">Непопулярний</string>
<string name="low_rating">Низький рейтинг</string>
<string name="sort_order_asc">За зростанням</string>
<string name="sort_order_desc">За спаданням</string>
<string name="by_date">Дата</string>
<string name="popularity">Популярність</string>
<string name="scrobbler_auth_required">Увійдіть у %s, щоб продовжити</string>
<string name="scrobbler_auth_intro">Увійдіть, щоб настроїти інтеграцію з %s. Це дозволить вам відстежувати прогрес та статус читання манги</string>
<string name="unstable_feature">Нестабільна функція</string>
<string name="unstable_feature_summary">Ця функція є експериментальною. Переконайтеся, що у вас є резервна копія, щоб уникнути втрати даних</string>
<string name="downloads_background">Фонові завантаження</string>
<string name="download_new_chapters">Завантажити нові розділи</string>
<string name="manga_with_downloaded_chapters">Манга із завантаженими розділами</string>
<string name="manga_replaced">Манґу «%1$s» (%2$s) замінено на «%3$s» (%4$s)</string>
<string name="fixing_manga">Виправлення манґи</string>
<string name="fixed">Виправлено успішно</string>
<string name="no_fix_required">Виправлення для «%s» не потрібно</string>
<string name="no_alternatives_found">Альтернативи для «%s» не знайдені</string>
<string name="manga_fix_prompt">Ця функція знайде альтернативні джерела обраної манги. Завдання займе деякий час і виконуватиметься у фоновому режимі</string>
<string name="content_type_novel">Новела</string>
<string name="content_type_manhua">Маньхуа</string>
<string name="content_type_manhwa">Манхва</string>
<string name="recently_added">Нещодавно додано</string>
<string name="added_long_ago">Додано давно</string>
<string name="popular_in_hour">Популярне в цю годину</string>
<string name="popular_today">Популярно сьогодні</string>
<string name="popular_in_week">Популярно цього тижня</string>
<string name="popular_in_month">Популярно цього місяця</string>
<string name="popular_in_year">Популярно цього року</string>
<string name="original_language">Вихідна мова</string>
<string name="year">Рік</string>
<string name="demographics">Демографія</string>
<string name="demographic_shounen">Шьонен</string>
<string name="demographic_shoujo">Шьоджьо</string>
<string name="demographic_seinen">Сейнен</string>
<string name="demographic_josei">Джьосей</string>
<string name="years">Роки</string>
<string name="any">Будь-який</string>
<string name="filter_search_warning">Це джерело не підтримує пошук з фільтрами. Ваші фільтри були очищені</string>
<string name="demographic_kodomo">Кодомо</string>
<string name="content_type_one_shot">Ваншот</string>
<string name="content_type_doujinshi">Додзінсі</string>
<string name="content_type_image_set">Набір зображень</string>
<string name="content_type_artist_cg">Художник CG</string>
<string name="content_type_game_cg">Ігрова CG</string>
<string name="debug">Налагодження</string>
<string name="source_code">Вихідний код</string>
<string name="user_manual">Посібник користувача</string>
<string name="telegram_group">Група у Telegram</string>
<string name="error_image_format">Формат зображення, що не підтримується: %s</string>
<string name="error_not_image">Невірний формат: очікувалося зображення, але отримано %s</string>
<string name="start_download">Почати завантаження</string>
<string name="save_manga_confirm">Зберегти вибрану мангу? Це може зайняти трафік та місце на диску</string>
<string name="save_manga">Зберегти манґу</string>
<string name="genre">Жанр</string>
<string name="download_added">Завантаження додано</string>
<string name="more_options">Більше опцій</string>
<string name="destination_directory">Каталог призначення</string>
<string name="chapter_selection_hint">Ви можете вибрати розділи для завантаження, утримуючи елемент у списку розділів.</string>
<string name="chapters_all">Усі</string>
<string name="download_over_cellular">Завантаження через мобільну мережу</string>
<string name="download_cellular_confirm">Дозволити завантаження через мобільну мережу?</string>
<string name="dont_allow">Не дозволяти</string>
<string name="allow_always">Дозволити завжди</string>
<string name="allow_once">Дозволити один раз</string>
<string name="ask_every_time">Запитувати щоразу</string>
<string name="screen_orientation">Орієнтація екрану</string>
<string name="portrait">Портретна</string>
<string name="landscape">Ландшафтна</string>
<string name="access_denied_403">Доступ був відхилений (403)</string>
<string name="max_backups_count">Максимальна кількість резервних копій</string>
<string name="delete_old_backups">Видалити старі резервні копії</string>
<string name="delete_old_backups_summary">Автоматично видаляти старі резервні копії, щоб звільнити місце для даних</string>
<string name="handle_links">Обробка посилань</string>
<string name="email">Електронна пошта</string>
<string name="captcha_required_message">Це джерело вимагає рішення капчі, щоб продовжити</string>
<string name="author">Автор</string>
<string name="rating">Рейтинг</string>
<string name="source">Джерело</string>
<string name="translation">Переклад</string>
<string name="show_slider">Показати слайдер</string>
<string name="incognito">Інкогніто</string>
<string name="error_connection_reset">Скидання з\'єднання віддаленим хостом</string>
<string name="backup_tg_check">Перевірте, чи працює API</string>
<string name="backup_tg_echo">Тестове повідомлення</string>
<string name="backup_tg_id_not_set">ID чату не встановлено</string>
<string name="telegram_chat_id">ID чату Telegram</string>
<string name="open_telegram_bot">Відкрийте Telegram-бота</string>
<string name="send_backups_telegram">Надсилати резервні копії до Telegram</string>
<string name="test_connection">Тестове з\'єднання</string>
<string name="telegram_chat_id_summary">Введіть ID чату, куди слід надсилати резервні копії</string>
<string name="open_telegram_bot_summary">Натисніть, щоб відкрити чат з Kotatsu Backup Bot</string>
<string name="clear_database">Очистити базу даних</string>
<string name="clear_database_summary">Видалити інформацію про мангу, яка не використовується</string>
<string name="enable_all_sources">Включити всі джерела манги</string>
<string name="enable_all_sources_summary">Усі доступні джерела манги будуть включені назавжди</string>
<string name="all_sources_enabled">Усі джерела включені</string>
<string name="reader_info_bar_transparent">Прозора інформаційна панель читання</string>
<string name="backup_restored_background">Резервну копію буде відновлено у фоновому режимі</string>
<string name="restoring_backup">Відновлення резервної копії</string>
<string name="reader_controls_in_bottom_bar">Елементи керування читанням на нижній панелі</string>
<string name="chapters_and_pages">Розділи та сторінки</string>
<string name="pages_slider">Повзунок перемикання сторінок</string>
<string name="screen_rotation_locked">Поворот екрана було заблоковано</string>
<string name="screen_rotation_unlocked">Поворот екрана було розблоковано</string>
<string name="badges_in_lists">Піктограми у списках</string>
<string name="search_everywhere">Пошук усюди</string>
<string name="simple">Простий</string>
<string name="global_search">Глобальний пошук</string>
<string name="disable_captcha_notifications">Вимкнути сповіщення про CAPTCHA</string>
<string name="chapter_volume_number">Том %1$s Розділ %2$s</string>
<string name="chapter_number">Розділ %s</string>
<string name="unnamed_chapter">Безіменний розділ</string>
<string name="search_disabled_sources">Пошук за відключеними джерелами</string>
<string name="error_details">Подробиці помилки</string>
<string name="book_effect">Жовтий фон (фільтр синього)</string>
<string name="theme_name_totoro">Тоторо</string>
<string name="theme_name_itsuka">Іцука</string>
<string name="reader_multitask_summary">Дозволяє тримати відкритими кілька читалок із різною мангою одночасно</string>
<string name="reader_multitask">Відкривати читалку як окреме завдання</string>
<string name="share_backup">Поділитись резервною копією</string>
<string name="creating_backup">Створення резервної копії</string>
<string name="collapse_long_description">Згорнути довгий опис</string>
<string name="adblock_summary">Блокування реклами у вбудованому браузері (бета)</string>
<string name="adblock">Блокувати рекламу у браузері</string>
<string name="expand">Розширювати</string>
<string name="collapse">Згорнутвання</string>
<string name="changelog_summary">Історія змін для нещодавно випущених версій</string>
<string name="changelog">Журнал змін</string>
<string name="hide_from_main_screen">Сховати з головного екрану</string>
<string name="additional_action_required">Потрібні додаткові дії</string>
<string name="incognito_for_nsfw">Режим інкогніто для NSFW-манґи</string>
<string name="incognito_mode_hint_nsfw">Ця манґа може містити контент для дорослих. Бажаєте використовувати режим інкогніто?</string>
<string name="dont_ask_again">Більше не питати</string>
<string name="page_switch_timer">Сторінка змінюватиметься кожні ~%d секунд</string>
<string name="change_cover">Змінити обкладинку</string>
<string name="pick_custom_file">Вибрати файл користувача</string>
<string name="pick_manga_page">Вибрати сторінку манґи</string>
<string name="use_default_cover">Використовувати стандартну обкладинку</string>
<string name="manga_override_hint">Ці зміни впливатимуть на те, як манга відображається у програмі</string>
<string name="error_non_file_uri">Вибраний шлях не може бути використаний, оскільки він не позначає файл чи каталог</string>
<string name="tags_warnings_summary">Виділяти жанри, які можуть бути неприйнятними для більшості користувачів</string>
<string name="tags_warnings">Виділяти небезпечні жанри</string>
<string name="suggestions_disabled_sources_summary">Відображати рекомендації з усіх джерел манги, включаючи вимкнені</string>
<string name="include_disabled_sources">Увімкнути відключені джерела</string>
<string name="exclude_nsfw_from_suggestions_summary">Доросла манґа не відображатиметься у рекомендаціях. Ця опція може не працювати з деякими джерелами</string>
<string name="no_write_permission_to_file">Немає прав на запис до файлу</string>
<string name="clear_browser_data">Очистити дані браузера</string>
<string name="link_to_manga_in_app">Посилання на мангу у Kotatsu</string>
<string name="link_to_manga_on_s">Посилання на мангу на %s</string>
<string name="error_disclaimer_report">Ви можете надіслати звіт про помилку розробникам. Це допоможе нам виправити проблему.</string>
<string name="error_disclaimer_app_outdated">Схоже, що ваша версія Kotatsu застаріла. Будь ласка, установіть останню версію, щоб отримати всі доступні виправлення.</string>
<string name="error_disclaimer_manga">Спробуйте відкрити манґу в браузері, щоб переконатися, що вона доступна джерелом.</string>
</resources>

View File

@@ -824,7 +824,7 @@
<string name="adblock">Chặn quảng cáo trong trình duyệt (Webview)</string>
<string name="adblock_summary">Chặn những quảng cáo xuất hiện trong trình duyệt Webview của hệ thống khi duyệt (Beta)</string>
<string name="collapse_long_description">Thu gọn đoạn mô tả dài</string>
<string name="creating_backup">Đang tạo sao lưu</string>
<string name="creating_backup">Sao lưu dữ liệu</string>
<string name="share_backup">Chia sẻ dữ liệu đã sao lưu</string>
<string name="reader_multitask">Mở trình đọc sang 1 tác vụ riêng biệt</string>
<string name="reader_multitask_summary">Tách trình đọc thành 1 tác vụ riêng biệt sẽ cho bạn khả năng mở và đọc nhiều truyện cùng một lúc (tốt hơn với máy tính bảng, Chrome OS và các giả lập Android cho phép mở đa cửa sổ trên máy tính như WSA,...)</string>
@@ -832,4 +832,7 @@
<string name="theme_name_totoro">Totoro</string>
<string name="reader_navigation_inverted">Đảo ngược phím điều khiển hướng đọc</string>
<string name="reader_navigation_inverted_summary">Đảo ngược hướng của các phím điều khiển để đọc truyện như nút tăng / giảm âm lượng, D-pad (lên / xuống / trái / phải)</string>
<string name="book_effect">Làm dịu ánh màu xanh của ảnh (chuyển sang màu vàng)</string>
<string name="local_storage_cleanup">Dọn dẹp bộ nhớ lưu trữ cục bộ</string>
<string name="packup_creation_failed">Tạo bản sao lưu dữ liệu thất bại</string>
</resources>

View File

@@ -854,4 +854,6 @@
<string name="theme_name_itsuka">Itsuka</string>
<string name="theme_name_totoro">Totoro</string>
<string name="book_effect">Yellowish background (blue filter)</string>
<string name="local_storage_cleanup">Local storage cleanup</string>
<string name="packup_creation_failed">Failed to create backup</string>
</resources>

View File

@@ -13,6 +13,7 @@ constraintlayout = "2.2.1"
coreKtx = "1.16.0"
coroutines = "1.10.2"
dagger = "2.56.2"
decoroutinator = "2.5.4"
desugar = "2.1.5"
diskLruCache = "1.5"
documentfile = "1.1.0"
@@ -32,7 +33,7 @@ material = "1.14.0-alpha02"
moshi = "1.15.2"
okhttp = "4.12.0"
okio = "3.12.0"
parsers = "248d51321a"
parsers = "613d1cd1a1"
preference = "1.2.1"
recyclerview = "1.4.0"
room = "2.7.2"
@@ -119,3 +120,4 @@ kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinx-serizliation = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "room" }
decoroutinator = { id = "dev.reformator.stacktracedecoroutinator", version.ref = "decoroutinator" }