Compare commits

...

17 Commits
v5.0 ... v5.0.2

Author SHA1 Message Date
Koitharu
12e9fb5aab Fix download error with empty largeCoverUrl 2023-05-06 17:41:15 +03:00
Koitharu
5fc08d9ecb Update parsers 2023-05-06 17:34:46 +03:00
InfinityDouki56
e7eb61e3e5 Translated using Weblate (Filipino)
Currently translated at 91.4% (398 of 435 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-05-06 17:34:29 +03:00
vividly
dae3982e67 Translated using Weblate (Japanese)
Currently translated at 97.0% (422 of 435 strings)

Co-authored-by: vividly <vividly@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-05-06 17:34:29 +03:00
Макар Разин
45b2d2bebe Translated using Weblate (Belarusian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2023-05-06 17:34:29 +03:00
Zakhar Timoshenko
77fa34835f Fix remaining incorrect sample data 2023-05-06 17:23:28 +03:00
Zakhar Timoshenko
cee68069d6 Remove sample data 2023-05-06 17:19:41 +03:00
CakesTwix
48afc8624b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-05-06 16:07:11 +03:00
Koitharu
b9b41ed491 Fix local manga list update on deletion 2023-05-06 16:05:39 +03:00
Zakhar Timoshenko
427272aac1 Update Material components to 1.9.0 2023-05-05 19:19:41 +03:00
Koitharu
78f417ebe1 Fix downloading chapters with the same names 2023-05-01 12:52:35 +03:00
Koitharu
3fd6bec433 Update parsers 2023-04-28 17:24:21 +03:00
Koitharu
262e26a0cc Fix crashes 2023-04-28 17:23:29 +03:00
Koitharu
1b64c2a330 Update parsers 2023-04-28 17:23:29 +03:00
Koitharu
5ea0ecbd12 Permormance improvements 2023-04-28 17:23:29 +03:00
Dawid Jarubas
f9a1d1617e Translated using Weblate (Polish)
Currently translated at 91.4% (398 of 435 strings)

Co-authored-by: Dawid Jarubas <jarubas.dawid@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2023-04-28 17:22:58 +03:00
gallegonovato
10d8365fc1 Translated using Weblate (Spanish)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-04-28 17:22:58 +03:00
93 changed files with 864 additions and 887 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 538
versionName '5.0'
versionCode 540
versionName '5.0.2'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -78,7 +78,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:306d46ea93') {
implementation('com.github.KotatsuApp:kotatsu-parsers:96b9ac36f3') {
exclude group: 'org.json', module: 'json'
}
@@ -100,7 +100,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.material:material:1.9.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

View File

@@ -1,10 +0,0 @@
Slice of Life, Mystery
Slice of Life, Mystery
Psychological, Romance, Comedy, Slice of Life, Supernatural
Sci-Fi, Comedy
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Reincarnation, Sci-Fi, Historical, Psychological, Drama, Slice of Life, Supernatural, Mystery
Adventure, Slice of Life, Mystery
Adventure, Slice of Life, Mystery

View File

@@ -1,10 +0,0 @@
Forget-me-not Vol. 1
Forget-me-not Vol. 2
La Pomme Prisoinniere
Momo Kanchou no Himitsu Kichi
Omoide Emanon
Sasurai Emanon Vol. 1
Sasurai Emanon Vol. 2
Sasurai Emanon Vol. 3
Wandering Island Vol. 1
Wandering Island Vol. 2

View File

@@ -48,7 +48,7 @@ class BookmarksViewModel @Inject constructor(
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
launchJob(Dispatchers.Default) {
val handle = repository.removeBookmarks(ids)
onActionDone.postCall(ReversibleAction(R.string.bookmarks_removed, handle))
onActionDone.emitCall(ReversibleAction(R.string.bookmarks_removed, handle))
}
}
}

View File

@@ -20,6 +20,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
@@ -40,6 +43,8 @@ import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
@@ -205,5 +210,16 @@ interface AppModule {
MemoryContentCache(application)
}
}
@Provides
@Singleton
@LocalStorageChanges
fun provideMutableLocalStorageChangesFlow(): MutableSharedFlow<LocalManga?> = MutableSharedFlow()
@Provides
@LocalStorageChanges
fun provideLocalStorageChangesFlow(
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
): SharedFlow<LocalManga?> = flow.asSharedFlow()
}
}

View File

@@ -5,5 +5,5 @@ import okio.IOException
class CloudFlareProtectedException(
val url: String,
val headers: Headers,
@Transient val headers: Headers,
) : IOException("Protected by CloudFlare")

View File

@@ -1,3 +1,3 @@
package org.koitharu.kotatsu.core.exceptions
class WrongPasswordException : SecurityException()
class WrongPasswordException : IllegalArgumentException()

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
@@ -95,7 +96,12 @@ class MangaPrefetchService : CoroutineIntentService() {
val intent = Intent(context, MangaPrefetchService::class.java)
intent.action = ACTION_PREFETCH_PAGES
intent.putExtra(EXTRA_CHAPTER, ParcelableMangaChapters(listOf(chapter)))
context.startService(intent)
try {
context.startService(intent)
} catch (e: IllegalStateException) {
// probably app is in background
e.printStackTraceDebug()
}
}
fun prefetchLast(context: Context) {

View File

@@ -1,9 +1,7 @@
package org.koitharu.kotatsu.details.ui
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.transition.Slide
import android.transition.TransitionManager
@@ -66,13 +64,6 @@ class DetailsActivity :
private val viewModel: DetailsViewModel by viewModels()
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val downloadedManga = DownloadService.getDownloadedManga(intent) ?: return
viewModel.onDownloadComplete(downloadedManga)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityDetailsBinding.inflate(layoutInflater))
@@ -130,7 +121,6 @@ class DetailsActivity :
}
viewModel.chapters.observe(this, PrefetchObserver(this))
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
addMenuProvider(
DetailsMenuProvider(
activity = this,
@@ -142,11 +132,6 @@ class DetailsActivity :
binding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
}
override fun onDestroy() {
unregisterReceiver(downloadReceiver)
super.onDestroy()
}
override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return
when (v.id) {

View File

@@ -9,7 +9,6 @@ import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -19,7 +18,6 @@ import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@@ -46,7 +44,6 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.crossfade
import org.koitharu.kotatsu.utils.ext.drawableTop
import org.koitharu.kotatsu.utils.ext.enqueueWith
@@ -55,8 +52,6 @@ import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.resolveDp
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.utils.ext.textAndVisible
import org.koitharu.kotatsu.utils.ext.toFileOrNull
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
import javax.inject.Inject
@@ -94,6 +89,7 @@ class DetailsFragment :
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
viewModel.localSize.observe(viewLifecycleOwner, ::onLocalSizeChanged)
}
override fun onItemClick(item: Bookmark, view: View) {
@@ -150,20 +146,9 @@ class DetailsFragment :
}
if (manga.source == MangaSource.LOCAL) {
infoLayout.textViewSource.isVisible = false
val file = manga.url.toUri().toFileOrNull()
if (file != null) {
viewLifecycleScope.launch {
val size = file.computeSize()
infoLayout.textViewSize.text = FileSize.BYTES.format(requireContext(), size)
infoLayout.textViewSize.isVisible = true
}
} else {
infoLayout.textViewSize.isVisible = false
}
} else {
infoLayout.textViewSource.text = manga.source.title
infoLayout.textViewSource.isVisible = true
infoLayout.textViewSize.isVisible = false
}
infoLayout.textViewNsfw.isVisible = manga.isNsfw
@@ -192,6 +177,16 @@ class DetailsFragment :
}
}
private fun onLocalSizeChanged(size: Long) {
val textView = binding.infoLayout.textViewSize
if (size == 0L) {
textView.isVisible = false
} else {
textView.text = FileSize.BYTES.format(textView.context, size)
textView.isVisible = true
}
}
private fun onHistoryChanged(history: HistoryInfo) {
binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
}

View File

@@ -4,6 +4,7 @@ import android.text.Html
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.net.toUri
import androidx.core.text.getSpans
import androidx.core.text.parseAsHtml
import androidx.lifecycle.LiveData
@@ -14,6 +15,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
@@ -36,6 +38,8 @@ import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -46,8 +50,10 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.toFileOrNull
import java.io.IOException
import javax.inject.Inject
@@ -62,6 +68,7 @@ class DetailsViewModel @Inject constructor(
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val imageGetter: Html.ImageGetter,
private val delegate: MangaDetailsDelegate,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
) : BaseViewModel() {
private var loadingJob: Job
@@ -109,6 +116,23 @@ class DetailsViewModel @Inject constructor(
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val localSize = combine(
delegate.manga,
delegate.relatedManga,
) { m1, m2 ->
val url = when {
m1?.source == MangaSource.LOCAL -> m1.url
m2?.source == MangaSource.LOCAL -> m2.url
else -> null
}
if (url != null) {
val file = url.toUri().toFileOrNull()
file?.computeSize() ?: 0L
} else {
0L
}
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, 0)
val description = delegate.manga
.distinctUntilChangedBy { it?.description.orEmpty() }
.transformLatest {
@@ -174,6 +198,10 @@ class DetailsViewModel @Inject constructor(
init {
loadingJob = doLoad()
launchJob(Dispatchers.Default) {
localStorageChanges
.collect { onDownloadComplete(it) }
}
}
fun reload() {
@@ -195,7 +223,7 @@ class DetailsViewModel @Inject constructor(
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
onMangaRemoved.postCall(manga)
onMangaRemoved.emitCall(manga)
}
}
@@ -222,26 +250,6 @@ class DetailsViewModel @Inject constructor(
chaptersQuery.value = query?.trim().orEmpty()
}
fun onDownloadComplete(downloadedManga: Manga) {
val currentManga = delegate.manga.value ?: return
if (currentManga.id != downloadedManga.id) {
return
}
if (currentManga.source == MangaSource.LOCAL) {
reload()
} else {
viewModelScope.launch(Dispatchers.Default) {
runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga)
}.onSuccess {
delegate.relatedManga.value = it
}.onFailure {
it.printStackTraceDebug()
}
}
}
}
fun updateScrobbling(index: Int, rating: Float, status: ScrobblingStatus?) {
val scrobbler = getScrobbler(index) ?: return
launchJob(Dispatchers.Default) {
@@ -287,6 +295,27 @@ class DetailsViewModel @Inject constructor(
}
}
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
downloadedManga ?: return
val currentManga = delegate.manga.value ?: return
if (currentManga.id != downloadedManga.manga.id) {
return
}
if (currentManga.source == MangaSource.LOCAL) {
reload()
} else {
viewModelScope.launch(Dispatchers.Default) {
runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga.manga)
}.onSuccess {
delegate.relatedManga.value = it
}.onFailure {
it.printStackTraceDebug()
}
}
}
}
private fun Spanned.filterSpans(): CharSequence {
val spannable = SpannableString.valueOf(this)
val spans = spannable.getSpans<ForegroundColorSpan>()

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
@@ -29,6 +30,8 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.PausingHandle
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
@@ -38,6 +41,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
@@ -58,6 +62,7 @@ class DownloadManager @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val settings: AppSettings,
private val mangaRepositoryFactory: MangaRepository.Factory,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
) {
private val coverWidth = context.resources.getDimensionPixelSize(
@@ -120,9 +125,11 @@ class DownloadManager @Inject constructor(
outState.value = DownloadState.Preparing(startId, manga, cover)
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = LocalMangaOutput.getOrCreate(destination, data)
val coverUrl = data.largeCoverUrl ?: data.coverUrl
downloadFile(coverUrl, destination, tempFileName, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
val coverUrl = data.largeCoverUrl.ifNullOrEmpty { data.coverUrl }
if (coverUrl.isNotEmpty()) {
downloadFile(coverUrl, destination, tempFileName, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
}
}
val chapters = checkNotNull(
if (chaptersIdsSet == null) {
@@ -165,13 +172,18 @@ class DownloadManager @Inject constructor(
delay(SLOWDOWN_DELAY)
}
}
output.flushChapter(chapter)
if (output.flushChapter(chapter)) {
runCatchingCancellable {
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
}.onFailure(Throwable::printStackTraceDebug)
}
}
outState.value = DownloadState.PostProcessing(startId, data, cover)
output.mergeWithExisting()
output.finish()
val localManga = LocalMangaInput.of(output.rootFile).getManga().manga
outState.value = DownloadState.Done(startId, data, cover, localManga)
val localManga = LocalMangaInput.of(output.rootFile).getManga()
localStorageChanges.emit(localManga)
outState.value = DownloadState.Done(startId, data, cover, localManga.manga)
} catch (e: CancellationException) {
outState.value = DownloadState.Cancelled(startId, manga, cover)
throw e

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.download.ui.service
import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -56,7 +57,6 @@ class DownloadService : BaseService() {
override fun onCreate() {
super.onCreate()
isRunning = true
downloadNotification = DownloadNotification(this)
wakeLock = (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
@@ -93,7 +93,6 @@ class DownloadService : BaseService() {
if (wakeLock.isHeld) {
wakeLock.release()
}
isRunning = false
super.onDestroy()
}
@@ -205,12 +204,6 @@ class DownloadService : BaseService() {
companion object {
var isRunning: Boolean = false
private set
@Deprecated("Use LocalMangaRepository.watchReadableDirs instead")
const val ACTION_DOWNLOAD_COMPLETE = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_COMPLETE"
private const val ACTION_DOWNLOAD_CANCEL = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_CANCEL"
private const val ACTION_DOWNLOAD_RESUME = "${BuildConfig.APPLICATION_ID}.action.ACTION_DOWNLOAD_RESUME"
@@ -259,13 +252,6 @@ class DownloadService : BaseService() {
fun getResumeIntent(startId: Int) = Intent(ACTION_DOWNLOAD_RESUME)
.putExtra(EXTRA_CANCEL_ID, startId)
fun getDownloadedManga(intent: Intent?): Manga? {
if (intent?.action == ACTION_DOWNLOAD_COMPLETE) {
return intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
}
return null
}
private fun showStartedSnackbar(view: View) {
Snackbar.make(view, R.string.download_started, Snackbar.LENGTH_LONG)
.setAction(R.string.details) {

View File

@@ -54,7 +54,7 @@ class ExploreViewModel @Inject constructor(
fun openRandom() {
launchLoadingJob(Dispatchers.Default) {
val manga = exploreRepository.findRandomManga(tagsLimit = 8)
onOpenManga.postCall(manga)
onOpenManga.emitCall(manga)
}
}
@@ -64,7 +64,7 @@ class ExploreViewModel @Inject constructor(
val rollback = ReversibleHandle {
settings.hiddenSources -= source.name
}
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback))
}
}

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEdit
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.NO_ID
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.emitValue
import javax.inject.Inject
@HiltViewModel
@@ -34,7 +35,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
init {
launchLoadingJob(Dispatchers.Default) {
category.postValue(
category.emitValue(
if (categoryId != NO_ID) {
repository.getCategory(categoryId)
} else {
@@ -57,7 +58,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
} else {
repository.updateCategory(categoryId, title, sortOrder, isTrackerEnabled, isVisibleOnShelf)
}
onSaved.postCall(Unit)
onSaved.emitCall(Unit)
}
}
}

View File

@@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
@@ -28,7 +27,6 @@ import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
@HiltViewModel
@@ -43,9 +41,6 @@ class FavouritesListViewModel @Inject constructor(
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
var categoryName: String? = null
private set
val sortOrder: LiveData<SortOrder?> = if (categoryId == NO_ID) {
MutableLiveData(null)
} else {
@@ -82,18 +77,6 @@ class FavouritesListViewModel @Inject constructor(
emit(listOf(it.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
if (categoryId != NO_ID) {
launchJob {
categoryName = withContext(Dispatchers.Default) {
runCatchingCancellable {
repository.getCategory(categoryId).title
}.getOrNull()
}
}
}
}
override fun onRefresh() = Unit
override fun onRetry() = Unit
@@ -108,7 +91,7 @@ class FavouritesListViewModel @Inject constructor(
} else {
repository.removeFromCategory(categoryId, ids)
}
onActionDone.postCall(ReversibleAction(R.string.removed_from_favourites, handle))
onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle))
}
}

View File

@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.emitValue
import org.koitharu.kotatsu.utils.ext.onFirst
import java.util.Date
import java.util.concurrent.TimeUnit
@@ -45,7 +46,7 @@ class HistoryListViewModel @Inject constructor(
val isGroupingEnabled = MutableLiveData<Boolean>()
private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled }
.onEach { isGroupingEnabled.postValue(it) }
.onEach { isGroupingEnabled.emitValue(it) }
override val content = combine(
repository.observeAllWithHistory(),
@@ -77,7 +78,7 @@ class HistoryListViewModel @Inject constructor(
override fun onRetry() = Unit
fun clearHistory() {
launchLoadingJob {
launchLoadingJob(Dispatchers.Default) {
repository.clear()
}
}
@@ -88,7 +89,7 @@ class HistoryListViewModel @Inject constructor(
}
launchJob(Dispatchers.Default) {
val handle = repository.delete(ids)
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle))
}
}

View File

@@ -17,11 +17,9 @@ import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager
import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager
@@ -30,7 +28,7 @@ import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.ListMode
@@ -46,7 +44,6 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity
@@ -127,7 +124,7 @@ abstract class MangaListFragment :
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
viewModel.onActionDone.observe(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
}
override fun onDestroyView() {
@@ -173,17 +170,6 @@ abstract class MangaListFragment :
listAdapter?.setItems(list, listCommitCallback)
}
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.show()
}
private fun resolveException(e: Throwable) {
if (ExceptionResolver.canResolve(e)) {
viewLifecycleScope.launch {

View File

@@ -1,3 +1,5 @@
@file:androidx.annotation.OptIn(ExperimentalBadgeUtils::class)
package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
@@ -5,6 +7,7 @@ import androidx.annotation.CheckResult
import androidx.core.view.doOnNextLayout
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import org.koitharu.kotatsu.R
@CheckResult

View File

@@ -1,12 +1,19 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun emptyHintAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: ListStateHolderListener,
) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>(
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
@@ -15,9 +22,13 @@ fun emptyHintAD(
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
bind {
binding.icon.setImageResource(item.icon)
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
binding.textPrimary.setText(item.textPrimary)
binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
}
onViewRecycled {
binding.icon.disposeImageRequest()
}
}

View File

@@ -1 +0,0 @@
package org.koitharu.kotatsu.local

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.local.data
import androidx.core.net.toFile
import androidx.core.net.toUri
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import java.io.File
@@ -9,6 +11,8 @@ class LocalManga(
val manga: Manga,
) {
constructor(manga: Manga) : this(manga.url.toUri().toFile(), manga)
var createdAt: Long = -1L
private set
get() {

View File

@@ -7,15 +7,10 @@ import androidx.annotation.WorkerThread
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.util.observe
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.getStorageName
@@ -36,6 +31,7 @@ class LocalStorageManager @Inject constructor(
val contentResolver: ContentResolver
get() = context.contentResolver
@WorkerThread
fun createHttpCache(): Cache {
val directory = File(context.externalCacheDir ?: context.cacheDir, "http")
directory.mkdirs()
@@ -80,14 +76,6 @@ class LocalStorageManager @Inject constructor(
fun getStorageDisplayName(file: File) = file.getStorageName(context)
fun observe(files: List<File>): Flow<File> {
if (files.isEmpty()) {
return emptyFlow()
}
return files.asFlow()
.flatMapMerge(files.size) { it.observe() }
}
@WorkerThread
private fun getConfiguredStorageDirs(): MutableSet<File> {
val set = getAvailableStorageDirs()

View File

@@ -84,7 +84,7 @@ class MangaIndex(source: String?) {
fun getCoverEntry(): String? = json.getStringOrNull("cover_entry")
fun addChapter(chapter: MangaChapter) {
fun addChapter(chapter: MangaChapter, filename: String?) {
val chapters = json.getJSONObject("chapters")
if (!chapters.has(chapter.id.toString())) {
val jo = JSONObject()
@@ -95,6 +95,7 @@ class MangaIndex(source: String?) {
jo.put("scanlator", chapter.scanlator)
jo.put("branch", chapter.branch)
jo.put("entries", "%08d_%03d\\d{3}".format(chapter.branch.hashCode(), chapter.number))
jo.put("file", filename)
chapters.put(chapter.id.toString(), jo)
}
}
@@ -103,6 +104,10 @@ class MangaIndex(source: String?) {
return json.getJSONObject("chapters").remove(id.toString()) != null
}
fun getChapterFileName(chapterId: Long): String? {
return json.optJSONObject("chapters")?.optJSONObject(chapterId.toString())?.getStringOrNull("file")
}
fun setCoverEntry(name: String) {
json.put("cover_entry", name)
}

View File

@@ -6,9 +6,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import org.koitharu.kotatsu.utils.ext.takeIfWriteable
@@ -20,47 +23,41 @@ import javax.inject.Singleton
@Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
private val cacheDir = checkNotNull(findSuitableDir(context)) {
val dirs = (context.externalCacheDirs + context.cacheDir).joinToString(";") {
it?.absolutePath.toString()
private val cacheDir = SuspendLazy {
val dirs = context.externalCacheDirs + context.cacheDir
dirs.firstNotNullOf {
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
}
"Cannot find any suitable directory for PagesCache: [$dirs]"
}
private val lruCache = createDiskLruCacheSafe(
dir = cacheDir,
size = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
)
private val lruCache = SuspendLazy {
val dir = cacheDir.get()
val size = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
runCatchingCancellable {
DiskLruCache.create(dir, size)
}.recoverCatching { error ->
error.printStackTraceDebug()
dir.deleteRecursively()
dir.mkdir()
DiskLruCache.create(dir, size)
}.getOrThrow()
}
suspend fun get(url: String): File? = runInterruptible(Dispatchers.IO) {
lruCache.get(url)?.takeIfReadable()
suspend fun get(url: String): File? {
val cache = lruCache.get()
return runInterruptible(Dispatchers.IO) {
cache.get(url)?.takeIfReadable()
}
}
suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
val file = File(cacheDir.parentFile, url.longHashCode().toString())
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
try {
file.outputStream().use { out ->
inputStream.copyToSuspending(out)
}
lruCache.put(url, file)
lruCache.get().put(url, file)
} finally {
file.delete()
}
}
}
private fun createDiskLruCacheSafe(dir: File, size: Long): DiskLruCache {
return try {
DiskLruCache.create(dir, size)
} catch (e: Exception) {
dir.deleteRecursively()
dir.mkdir()
DiskLruCache.create(dir, size)
}
}
private fun findSuitableDir(context: Context): File? {
val dirs = context.externalCacheDirs + context.cacheDir
return dirs.firstNotNullOfOrNull {
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
}
}

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.local.data
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LocalStorageChanges

View File

@@ -6,11 +6,13 @@ import androidx.documentfile.provider.DocumentFile
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.utils.ext.copyToSuspending
@@ -23,16 +25,19 @@ import javax.inject.Inject
class SingleMangaImporter @Inject constructor(
@ApplicationContext private val context: Context,
private val storageManager: LocalStorageManager,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
) {
private val contentResolver = context.contentResolver
suspend fun import(uri: Uri, progressState: MutableStateFlow<Float>?): LocalManga {
return if (isDirectory(uri)) {
val result = if (isDirectory(uri)) {
importDirectory(uri, progressState)
} else {
importFile(uri, progressState)
}
localStorageChanges.emit(result)
return result
}
private suspend fun importFile(uri: Uri, progressState: MutableStateFlow<Float>?): LocalManga {

View File

@@ -54,13 +54,14 @@ class LocalMangaDirOutput(
runInterruptible(Dispatchers.IO) {
output.put(name, file)
}
index.addChapter(chapter)
index.addChapter(chapter, chapterFileName(chapter))
}
override suspend fun flushChapter(chapter: MangaChapter) {
val output = chaptersOutput.remove(chapter) ?: return
override suspend fun flushChapter(chapter: MangaChapter): Boolean {
val output = chaptersOutput.remove(chapter) ?: return false
output.flushAndFinish()
flushIndex()
return true
}
override suspend fun finish() {
@@ -104,7 +105,18 @@ class LocalMangaDirOutput(
}
private fun chapterFileName(chapter: MangaChapter): String {
return "${chapter.number}_${chapter.name.toFileNameSafe()}".take(18) + ".cbz"
index.getChapterFileName(chapter.id)?.let {
return it
}
val baseName = "${chapter.number}_${chapter.name.toFileNameSafe()}".take(18)
var i = 0
while (true) {
val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz"
if (!File(rootFile, name).exists()) {
return name
}
i++
}
}
private suspend fun flushIndex() = runInterruptible(Dispatchers.IO) {

View File

@@ -16,7 +16,7 @@ sealed class LocalMangaOutput(
abstract suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String)
abstract suspend fun flushChapter(chapter: MangaChapter)
abstract suspend fun flushChapter(chapter: MangaChapter): Boolean
abstract suspend fun finish()

View File

@@ -57,10 +57,10 @@ class LocalMangaZipOutput(
runInterruptible(Dispatchers.IO) {
output.put(name, file)
}
index.addChapter(chapter)
index.addChapter(chapter, null)
}
override suspend fun flushChapter(chapter: MangaChapter) = Unit
override suspend fun flushChapter(chapter: MangaChapter): Boolean = false
override suspend fun finish() {
runInterruptible(Dispatchers.IO) {
@@ -98,7 +98,7 @@ class LocalMangaZipOutput(
}
otherIndex?.getMangaInfo()?.chapters?.let { chapters ->
for (chapter in chapters) {
index.addChapter(chapter)
index.addChapter(chapter, null)
}
}
}

View File

@@ -1,48 +0,0 @@
package org.koitharu.kotatsu.local.data.util
import android.os.Build
import android.os.FileObserver
import androidx.annotation.RequiresApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import java.io.File
fun File.observe() = callbackFlow {
val observer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FlowFileObserverQ(this, this@observe)
} else {
FlowFileObserver(this, this@observe)
}
observer.startWatching()
awaitClose { observer.stopWatching() }
}.flowOn(Dispatchers.IO)
@RequiresApi(Build.VERSION_CODES.Q)
private class FlowFileObserverQ(
private val producerScope: ProducerScope<File>,
private val file: File,
) : FileObserver(file, CREATE or DELETE or CLOSE_WRITE) {
override fun onEvent(event: Int, path: String?) {
producerScope.trySendBlocking(
if (path == null) file else file.resolve(path),
)
}
}
@Suppress("DEPRECATION")
private class FlowFileObserver(
private val producerScope: ProducerScope<File>,
private val file: File,
) : FileObserver(file.absolutePath, CREATE or DELETE or CLOSE_WRITE) {
override fun onEvent(event: Int, path: String?) {
producerScope.trySendBlocking(
if (path == null) file else file.resolve(path),
)
}
}

View File

@@ -6,11 +6,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
@@ -34,7 +37,10 @@ import javax.inject.Singleton
private const val MAX_PARALLELISM = 4
@Singleton
class LocalMangaRepository @Inject constructor(private val storageManager: LocalStorageManager) : MangaRepository {
class LocalMangaRepository @Inject constructor(
private val storageManager: LocalStorageManager,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
) : MangaRepository {
override val source = MangaSource.LOCAL
private val locks = CompositeMutex<Long>()
@@ -84,13 +90,18 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
suspend fun delete(manga: Manga): Boolean {
val file = Uri.parse(manga.url).toFile()
return file.deleteAwait()
val result = file.deleteAwait()
if (result) {
localStorageChanges.emit(null)
}
return result
}
suspend fun deleteChapters(manga: Manga, ids: Set<Long>) {
lockManga(manga.id)
try {
LocalMangaUtil(manga).deleteChapters(ids)
localStorageChanges.emit(LocalManga(manga))
} finally {
unlockManga(manga.id)
}
@@ -106,21 +117,24 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
val files = getAllFiles()
val input = files.firstNotNullOfOrNull { file ->
LocalMangaInput.of(file).takeIf {
runCatchingCancellable {
it.getMangaInfo()
}.getOrNull()?.id == remoteManga.id
}
if (files.isEmpty()) {
return null
}
return input?.getManga()
}
suspend fun watchReadableDirs(): Flow<File> {
val filter = TempFileFilter()
val dirs = storageManager.getReadableDirs()
return storageManager.observe(dirs)
.filterNot { filter.accept(it, it.name) }
return channelFlow {
for (file in files) {
launch {
val mangaInput = LocalMangaInput.of(file)
runCatchingCancellable {
val mangaInfo = mangaInput.getMangaInfo()
if (mangaInfo != null && mangaInfo.id == remoteManga.id) {
send(mangaInput)
}
}.onFailure {
it.printStackTraceDebug()
}
}
}
}.firstOrNull()?.getManga()
}
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
@@ -149,7 +163,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
dirs.flatMap { dir ->
dir.listFiles(TempFileFilter())?.toList().orEmpty()
}.forEach { file ->
file.delete()
file.deleteRecursively()
}
}
return true

View File

@@ -9,10 +9,12 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.MutableSharedFlow
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -25,6 +27,10 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
@Inject
lateinit var localMangaRepository: LocalMangaRepository
@Inject
@LocalStorageChanges
lateinit var localStorageChanges: MutableSharedFlow<LocalManga?>
override fun onCreate() {
super.onCreate()
isRunning = true
@@ -41,10 +47,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
startForeground()
val mangaWithChapters = localMangaRepository.getDetails(manga)
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
sendBroadcast(
Intent(DownloadService.ACTION_DOWNLOAD_COMPLETE)
.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)),
)
localStorageChanges.emit(LocalManga(manga))
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}

View File

@@ -9,11 +9,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
@@ -27,6 +26,8 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader2
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -46,6 +47,7 @@ class LocalListViewModel @Inject constructor(
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
private val tagHighlighter: MangaTagHighlighter,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
) : MangaListViewModel(settings), ListExtraProvider {
val onMangaRemoved = SingleLiveEvent<Unit>()
@@ -83,7 +85,14 @@ class LocalListViewModel @Inject constructor(
init {
onRefresh()
watchDirectories()
launchJob(Dispatchers.Default) {
localStorageChanges
.collectLatest {
if (refreshJob?.isActive != true) {
doRefresh()
}
}
}
}
override fun onUpdateFilter(tags: Set<MangaTag>) {
@@ -108,21 +117,19 @@ class LocalListViewModel @Inject constructor(
}
fun delete(ids: Set<Long>) {
launchLoadingJob {
withContext(Dispatchers.Default) {
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.update { list ->
list?.filterNot { it.id == manga.id }
}
launchLoadingJob(Dispatchers.Default) {
val itemsToRemove = checkNotNull(mangaList.value).filter { it.id in ids }
for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file")
runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original)
}
mangaList.update { list ->
list?.filterNot { it.id == manga.id }
}
}
onMangaRemoved.call(Unit)
onMangaRemoved.emitCall(Unit)
}
}
@@ -137,15 +144,6 @@ class LocalListViewModel @Inject constructor(
}
}
private fun watchDirectories() {
viewModelScope.launch(Dispatchers.Default) {
repository.watchReadableDirs()
.collectLatest {
doRefresh()
}
}
}
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
val tags = HashMap<MangaTag, Int>()
for (item in mangaList) {

View File

@@ -60,9 +60,9 @@ class MainViewModel @Inject constructor(
}
fun openLastReader() {
launchLoadingJob {
launchLoadingJob(Dispatchers.Default) {
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
onOpenReader.call(manga)
onOpenReader.emitCall(manga)
}
}
}

View File

@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.emitValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireValue
@@ -202,12 +203,12 @@ class ReaderViewModel @Inject constructor(
prevJob?.cancelAndJoin()
try {
val dest = pageSaveHelper.savePage(pageLoader, page, saveLauncher)
onPageSaved.postCall(dest)
onPageSaved.emitCall(dest)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
e.printStackTraceDebug()
onPageSaved.postCall(null)
onPageSaved.emitCall(null)
}
}
}
@@ -285,7 +286,7 @@ class ReaderViewModel @Inject constructor(
percent = computePercent(state.chapterId, state.page),
)
bookmarksRepository.addBookmark(bookmark)
onShowToast.postCall(R.string.bookmark_added)
onShowToast.emitCall(R.string.bookmark_added)
}
}
@@ -322,7 +323,7 @@ class ReaderViewModel @Inject constructor(
val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch
mangaData.value = manga.filterChapters(branch)
readerMode.postValue(mode)
readerMode.emitValue(mode)
chaptersLoader.loadSingleChapter(manga, requireNotNull(currentState.value).chapterId)
// save state
@@ -333,7 +334,7 @@ class ReaderViewModel @Inject constructor(
}
}
notifyStateChanged()
content.postValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
content.emitValue(ReaderContent(chaptersLoader.snapshot(), currentState.value))
}
}
@@ -341,7 +342,7 @@ class ReaderViewModel @Inject constructor(
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
loadingJob = launchLoadingJob(Dispatchers.Default) {
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
content.postValue(ReaderContent(chaptersLoader.snapshot(), null))
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
}
}

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity.Companion.EXTRA_MANGA
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.emitValue
import javax.inject.Inject
@HiltViewModel
@@ -43,7 +44,7 @@ class ColorFilterConfigViewModel @Inject constructor(
launchLoadingJob(Dispatchers.Default) {
val repository = mangaRepositoryFactory.create(page.source)
val url = repository.getPageUrl(page)
preview.postValue(
preview.emitValue(
MangaPage(
id = page.id,
url = url,
@@ -71,7 +72,7 @@ class ColorFilterConfigViewModel @Inject constructor(
fun save() {
launchLoadingJob(Dispatchers.Default) {
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
onDismiss.postCall(Unit)
onDismiss.emitCall(Unit)
}
}
}

View File

@@ -161,7 +161,7 @@ class RemoteListViewModel @Inject constructor(
e.printStackTraceDebug()
listError.value = e
if (!mangaList.value.isNullOrEmpty()) {
errorEvent.postCall(e)
errorEvent.emitCall(e)
}
}
}

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.emitValue
import org.koitharu.kotatsu.utils.ext.onFirst
import org.koitharu.kotatsu.utils.ext.require
import javax.inject.Inject
@@ -51,22 +52,22 @@ class ScrobblerConfigViewModel @Inject constructor(
init {
scrobbler.user
.onEach { user.postValue(it) }
.onEach { user.emitValue(it) }
.launchIn(viewModelScope + Dispatchers.Default)
}
fun onAuthCodeReceived(authCode: String) {
launchLoadingJob(Dispatchers.Default) {
val newUser = scrobbler.authorize(authCode)
user.postValue(newUser)
user.emitValue(newUser)
}
}
fun logout() {
launchLoadingJob(Dispatchers.Default) {
scrobbler.logout()
user.postValue(null)
onLoggedOut.postCall(Unit)
user.emitValue(null)
onLoggedOut.emitCall(Unit)
}
}

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.emitValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.require
import org.koitharu.kotatsu.utils.ext.requireValue
@@ -135,7 +136,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
}
doneJob = launchJob(Dispatchers.Default) {
currentScrobbler.linkManga(manga.id, targetId)
onClose.postCall(Unit)
onClose.emitCall(Unit)
}
}
@@ -154,7 +155,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
try {
val info = currentScrobbler.getScrobblingInfoOrNull(manga.id)
if (info != null) {
selectedItemId.postValue(info.targetId)
selectedItemId.emitValue(info.targetId)
}
} finally {
loadList(append = false)

View File

@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.emitValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
@@ -96,7 +97,7 @@ class MultiSearchViewModel @Inject constructor(
listError.value = null
listData.value = emptyList()
loadingData.value = true
query.postValue(q)
query.emitValue(q)
searchImpl(q)
} catch (e: CancellationException) {
throw e

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import org.koitharu.kotatsu.utils.ext.emitValue
import javax.inject.Inject
private const val DEBOUNCE_TIMEOUT = 500L
@@ -97,7 +98,7 @@ class SearchSuggestionViewModel @Inject constructor(
buildSearchSuggestion(searchQuery, hiddenSources)
}.distinctUntilChanged()
.onEach {
suggestion.postValue(it)
suggestion.emitValue(it)
}.launchIn(viewModelScope + Dispatchers.Default)
}

View File

@@ -15,9 +15,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
import java.io.FileOutputStream
import kotlin.math.roundToInt
@AndroidEntryPoint
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
@@ -66,13 +66,13 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
dismiss()
}
private fun onProgressChanged(progress: Progress?) {
private fun onProgressChanged(value: Float) {
with(binding.progressBar) {
isIndeterminate = progress == null
isVisible = true
if (progress != null) {
this.max = progress.total
this.progress = progress.value
val wasIndeterminate = isIndeterminate
isIndeterminate = value < 0
if (value >= 0) {
setProgressCompat((value * max).roundToInt(), !wasIndeterminate)
}
}
}

View File

@@ -4,13 +4,12 @@ import android.content.Context
import androidx.lifecycle.MutableLiveData
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
import javax.inject.Inject
@HiltViewModel
class BackupViewModel @Inject constructor(
@@ -18,7 +17,7 @@ class BackupViewModel @Inject constructor(
@ApplicationContext context: Context,
) : BaseViewModel() {
val progress = MutableLiveData<Progress?>(null)
val progress = MutableLiveData(-1f)
val onBackupDone = SingleLiveEvent<File>()
init {
@@ -26,18 +25,18 @@ class BackupViewModel @Inject constructor(
val file = BackupZipOutput(context).use { backup ->
backup.put(repository.createIndex())
progress.value = Progress(0, 3)
progress.value = 0f
backup.put(repository.dumpHistory())
progress.value = Progress(1, 3)
progress.value = 0.3f
backup.put(repository.dumpCategories())
progress.value = Progress(2, 3)
progress.value = 0.6f
backup.put(repository.dumpFavourites())
progress.value = Progress(3, 3)
progress.value = 0.9f
backup.finish()
progress.value = null
progress.value = 1f
backup.close()
backup.file
}

View File

@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
import org.koitharu.kotatsu.utils.progress.Progress
import kotlin.math.roundToInt
@AndroidEntryPoint
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
@@ -51,13 +51,13 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
dismiss()
}
private fun onProgressChanged(progress: Progress?) {
private fun onProgressChanged(value: Float) {
with(binding.progressBar) {
isVisible = true
isIndeterminate = progress == null
if (progress != null) {
this.max = progress.total
this.progress = progress.value
val wasIndeterminate = isIndeterminate
isIndeterminate = value < 0
if (value >= 0) {
setProgressCompat((value * max).roundToInt(), !wasIndeterminate)
}
}
}

View File

@@ -14,7 +14,6 @@ import org.koitharu.kotatsu.core.backup.BackupZipInput
import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.toUriOrNull
import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
import java.io.FileNotFoundException
import javax.inject.Inject
@@ -26,7 +25,7 @@ class RestoreViewModel @Inject constructor(
@ApplicationContext context: Context,
) : BaseViewModel() {
val progress = MutableLiveData<Progress?>(null)
val progress = MutableLiveData(-1f)
val onRestoreDone = SingleLiveEvent<CompositeResult>()
init {
@@ -47,16 +46,16 @@ class RestoreViewModel @Inject constructor(
try {
val result = CompositeResult()
progress.value = Progress(0, 3)
progress.value = 0f
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
progress.value = Progress(1, 3)
progress.value = 0.3f
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
progress.value = Progress(2, 3)
progress.value = 0.6f
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
progress.value = Progress(3, 3)
progress.value = 1f
onRestoreDone.call(result)
} finally {
backup.close()

View File

@@ -82,7 +82,7 @@ class SourcesListViewModel @Inject constructor(
val rollback = ReversibleHandle {
setEnabled(source, true)
}
onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
onActionDone.emitCall(ReversibleAction(R.string.source_disabled, rollback))
}
buildList()
}

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
import org.koitharu.kotatsu.core.db.removeObserverAsync
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.emitValue
import javax.inject.Inject
@HiltViewModel
@@ -31,7 +32,7 @@ class TrackerSettingsViewModel @Inject constructor(
private fun updateCategoriesCount() {
launchJob(Dispatchers.Default) {
categoriesCount.postValue(repository.getCategoriesCount())
categoriesCount.emitValue(repository.getCategoriesCount())
}
}

View File

@@ -5,13 +5,14 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
@@ -19,6 +20,8 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalManga
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -32,6 +35,7 @@ class ShelfRepository @Inject constructor(
private val historyRepository: HistoryRepository,
private val trackingRepository: TrackingRepository,
private val db: MangaDatabase,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
) {
fun observeShelfContent(): Flow<ShelfContent> = combine(
@@ -43,16 +47,15 @@ class ShelfRepository @Inject constructor(
ShelfContent(history, favorites, updated, local)
}
fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {
return flow {
emit(null)
emitAll(localMangaRepository.watchReadableDirs())
}.mapLatest {
localMangaRepository.getList(0, null, sortOrder)
}
private fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {
return localStorageChanges
.onStart { emit(null) }
.mapLatest {
localMangaRepository.getList(0, null, sortOrder)
}.distinctUntilChanged()
}
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
private fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
return db.favouriteCategoriesDao.observeAll()
.flatMapLatest { categories ->
val cats = categories.filter { it.isVisibleInLibrary }

View File

@@ -7,7 +7,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
@@ -60,10 +59,9 @@ class ShelfViewModel @Inject constructor(
repository.observeShelfContent(),
) { sections, isTrackerEnabled, isConnected, content ->
mapList(content, isTrackerEnabled, sections, isConnected)
}.debounce(500)
.catch { e ->
emit(listOf(e.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
}.catch { e ->
emit(listOf(e.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
launchJob(Dispatchers.Default) {
@@ -93,7 +91,7 @@ class ShelfViewModel @Inject constructor(
}
launchJob(Dispatchers.Default) {
val handle = favouritesRepository.removeFromCategory(category.id, ids)
onActionDone.postCall(ReversibleAction(R.string.removed_from_favourites, handle))
onActionDone.emitCall(ReversibleAction(R.string.removed_from_favourites, handle))
}
}
@@ -103,14 +101,14 @@ class ShelfViewModel @Inject constructor(
}
launchJob(Dispatchers.Default) {
val handle = historyRepository.delete(ids)
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
onActionDone.emitCall(ReversibleAction(R.string.removed_from_history, handle))
}
}
fun deleteLocal(ids: Set<Long>) {
launchLoadingJob(Dispatchers.Default) {
repository.deleteLocalManga(ids)
onActionDone.postCall(ReversibleAction(R.string.removal_completed, null))
onActionDone.emitCall(ReversibleAction(R.string.removal_completed, null))
}
}
@@ -123,7 +121,7 @@ class ShelfViewModel @Inject constructor(
historyRepository.deleteAfter(minDate)
R.string.removed_from_history
}
onActionDone.postCall(ReversibleAction(stringRes, null))
onActionDone.emitCall(ReversibleAction(stringRes, null))
}
}

View File

@@ -44,7 +44,7 @@ class ShelfAdapter(
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyHintAD(listener))
.addDelegate(emptyHintAD(coil, lifecycleOwner, listener))
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(errorStateListAD(listener))
}

View File

@@ -1,12 +1,12 @@
package org.koitharu.kotatsu.sync.ui
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.domain.SyncAuthResult
import org.koitharu.kotatsu.utils.SingleLiveEvent
import javax.inject.Inject
@HiltViewModel
class SyncAuthViewModel @Inject constructor(
@@ -19,7 +19,7 @@ class SyncAuthViewModel @Inject constructor(
launchLoadingJob(Dispatchers.Default) {
val token = api.authenticate(email, password)
val result = SyncAuthResult(email, password, token)
onTokenObtained.postCall(result)
onTokenObtained.emitCall(result)
}
}
}

View File

@@ -56,7 +56,7 @@ class FeedViewModel @Inject constructor(
if (clearCounters) {
repository.clearCounters()
}
onFeedCleared.postCall(Unit)
onFeedCleared.emitCall(Unit)
}
}

View File

@@ -1,45 +1,43 @@
package org.koitharu.kotatsu.utils
import androidx.collection.ArrayMap
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.LinkedList
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resume
class CompositeMutex<T : Any> : Set<T> {
private val data = ArrayMap<T, MutableList<CancellableContinuation<Unit>>>()
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()
private val mutex = Mutex()
override val size: Int
get() = data.size
get() = state.size
override fun contains(element: T): Boolean {
return data.containsKey(element)
return state.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> data.containsKey(x) }
return elements.all { x -> state.containsKey(x) }
}
override fun isEmpty(): Boolean {
return data.isEmpty
return state.isEmpty
}
override fun iterator(): Iterator<T> {
return data.keys.iterator()
return state.keys.iterator()
}
suspend fun lock(element: T) {
while (coroutineContext.isActive) {
waitForRemoval(element)
mutex.withLock {
if (data[element] == null) {
data[element] = LinkedList<CancellableContinuation<Unit>>()
if (state[element] == null) {
state[element] = MutableStateFlow(false)
return
}
}
@@ -47,23 +45,13 @@ class CompositeMutex<T : Any> : Set<T> {
}
fun unlock(element: T) {
val continuations = checkNotNull(data.remove(element)) {
checkNotNull(state.remove(element)) {
"CompositeMutex is not locked for $element"
}
continuations.forEach { c ->
if (c.isActive) {
c.resume(Unit)
}
}
}.value = true
}
private suspend fun waitForRemoval(element: T) {
val list = data[element] ?: return
suspendCancellableCoroutine { continuation ->
list.add(continuation)
continuation.invokeOnCancellation {
list.remove(continuation)
}
}
val flow = state[element] ?: return
flow.first { it }
}
}

View File

@@ -1,12 +1,18 @@
package org.koitharu.kotatsu.utils
import androidx.lifecycle.LiveData
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private const val DEFAULT_TIMEOUT = 5_000L
@@ -51,11 +57,16 @@ class FlowLiveData<T>(
private inner class Collector : FlowCollector<T> {
private var previousValue: Any? = value
private val dispatcher = Dispatchers.Main.immediate
override suspend fun emit(value: T) {
if (previousValue != value) {
previousValue = value
withContext(Dispatchers.Main.immediate) {
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
setValue(value)
}
} else {
setValue(value)
}
}

View File

@@ -5,7 +5,10 @@ import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.EmptyCoroutineContext
class SingleLiveEvent<T> : LiveData<T>() {
@@ -33,4 +36,15 @@ class SingleLiveEvent<T> : LiveData<T>() {
fun postCall(newValue: T) {
postValue(newValue)
}
}
suspend fun emitCall(newValue: T) {
val dispatcher = Dispatchers.Main.immediate
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
setValue(newValue)
}
} else {
setValue(newValue)
}
}
}

View File

@@ -2,7 +2,11 @@ package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.BufferedObserver
import kotlin.coroutines.EmptyCoroutineContext
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
"LiveData value is null"
@@ -15,3 +19,14 @@ fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: Buffere
previous = it
}
}
suspend fun <T> MutableLiveData<T>.emitValue(newValue: T) {
val dispatcher = Dispatchers.Main.immediate
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
withContext(dispatcher) {
value = newValue
}
} else {
value = newValue
}
}

View File

@@ -1,11 +1,9 @@
package org.koitharu.kotatsu.utils.ext
import android.icu.lang.UCharacter.GraphemeClusterBreak.T
@Suppress("UNCHECKED_CAST")
fun <T> Class<T>.castOrNull(obj: Any?): T? {
if (obj == null || !isInstance(obj)) {
return null
}
return obj as T
}
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.image
import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Html
import androidx.annotation.WorkerThread
import coil.ImageLoader
import coil.executeBlocking
import coil.request.ImageRequest
@@ -14,6 +15,7 @@ class CoilImageGetter @Inject constructor(
private val coil: ImageLoader,
) : Html.ImageGetter {
@WorkerThread
override fun getDrawable(source: String?): Drawable? {
return coil.executeBlocking(
ImageRequest.Builder(context)

View File

@@ -1,25 +0,0 @@
package org.koitharu.kotatsu.utils.progress
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Deprecated("Should be replaced with Float")
@Parcelize
data class Progress(
val value: Int,
val total: Int,
) : Parcelable, Comparable<Progress> {
override fun compareTo(other: Progress): Int {
return if (this.total == other.total) {
this.value.compareTo(other.value)
} else {
this.part().compareTo(other.part())
}
}
val isIndeterminate: Boolean
get() = total <= 0
private fun part() = if (isIndeterminate) -1.0 else value / total.toDouble()
}

View File

@@ -51,7 +51,7 @@
app:shapeAppearance="?shapeAppearanceCornerLarge"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_before"
@@ -88,7 +88,7 @@
app:shapeAppearance="?shapeAppearanceCornerLarge"
app:strokeColor="?colorOutline"
app:strokeWidth="1dp"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_after"

View File

@@ -20,7 +20,9 @@
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp" />
android:layout_marginTop="6dp"
android:indeterminate="true"
android:max="100" />
<TextView
android:id="@+id/textView_subtitle"

View File

@@ -29,7 +29,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:background="@sample/covers[5]"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
@@ -53,7 +53,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/titles[5]" />
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/textView_subtitle"

View File

@@ -20,7 +20,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -33,7 +33,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"

View File

@@ -26,7 +26,7 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#99FFFFFF"
tools:src="@sample/covers"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#99FFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
@@ -44,7 +44,7 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
app:tintMode="src_atop"
tools:backgroundTint="#4DFFFFFF"
tools:src="@sample/covers"
tools:src="@tools:sample/backgrounds/scenic"
tools:tint="#4DFFFFFF" />
<com.google.android.material.imageview.ShapeableImageView
@@ -60,7 +60,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"

View File

@@ -23,7 +23,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -38,7 +38,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"

View File

@@ -18,7 +18,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -32,7 +32,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/textView_summary"

View File

@@ -27,7 +27,7 @@
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:ignore="ContentDescription"
tools:src="@sample/covers[5]" />
tools:src="@tools:sample/backgrounds/scenic[5]" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView"
@@ -50,7 +50,7 @@
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/thumbnail"
tools:text="@sample/titles[5]" />
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@@ -18,7 +18,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -32,7 +32,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/textView_subtitle"
@@ -46,6 +46,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toBottomOf="@+id/textView_title"
tools:text="@sample/genres" />
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -20,7 +20,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
android:id="@+id/progressView"
@@ -44,7 +44,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/textView_subtitle"

View File

@@ -21,7 +21,7 @@
android:layout_alignParentTop="true"
android:contentDescription="@null"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers[7]" />
tools:src="@tools:sample/backgrounds/scenic[7]" />
<TextView
android:id="@+id/textView_title"
@@ -36,7 +36,7 @@
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
app:drawableTint="?colorControlNormal"
tools:drawableEndCompat="@drawable/ic_shikimori"
tools:text="@sample/titles[5]" />
tools:text="@tools:sample/lorem" />
<RatingBar
android:id="@+id/ratingBar"

View File

@@ -18,7 +18,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -32,7 +32,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
<RatingBar
android:id="@+id/ratingBar"

View File

@@ -23,7 +23,7 @@
android:orientation="vertical"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -33,7 +33,7 @@
android:ellipsize="end"
android:lines="1"
android:textAppearance="?attr/textAppearanceLabelSmall"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@@ -24,7 +24,7 @@
android:layout_height="@dimen/widget_cover_height"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@sample/covers" />
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/textView_title"
@@ -35,7 +35,7 @@
android:lines="2"
android:padding="2dp"
android:textColor="?android:attr/textColorPrimary"
tools:text="@sample/titles" />
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@@ -33,7 +33,7 @@
app:layout_constraintTop_toBottomOf="@id/dragHandle"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:background="@sample/covers[9]"
tools:background="@tools:sample/backgrounds/scenic"
tools:ignore="ContentDescription,UnusedAttribute" />
<ImageView
@@ -61,7 +61,7 @@
app:layout_constraintEnd_toStartOf="@id/button_menu"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/dragHandle"
tools:text="@sample/titles[9]" />
tools:text="@tools:sample/lorem[9]" />
<ImageButton
android:id="@+id/button_menu"

View File

@@ -428,4 +428,6 @@
<string name="speed">Хуткасць</string>
<string name="restore_backup_description">Імпарт раней створанай рэзервовай копіі дадзеных карыстальніка</string>
<string name="show_on_shelf">Паказаць на паліцы</string>
<string name="find_similar">Знайсці падобныя</string>
<string name="sync_auth_hint">Вы можаце ўвайсці ў існуючы ўліковы запіс або стварыць новы</string>
</resources>

View File

@@ -429,4 +429,5 @@
<string name="show_on_shelf">Mostrar en la estantería</string>
<string name="got_it">Entendido</string>
<string name="sync_auth_hint">Puedes acceder a una cuenta existente o crear una nueva</string>
<string name="find_similar">Buscar similares</string>
</resources>

View File

@@ -421,4 +421,13 @@
<string name="domain">Domain</string>
<string name="light_indicator">LED indicator</string>
<string name="settings_apply_restart_required">Mangyaring i-restart ang application upang ilapat ang mga pagbabagong ito</string>
<string name="got_it">Nakuha ko</string>
<string name="sources_reorder_tip">I-tap at hawakan ang isang aytem upang muling ayusin ang mga ito</string>
<string name="restore_backup_description">Mag-import ng dating ginawa na backup ng data ng user</string>
<string name="show_on_shelf">Ipakita sa Istante</string>
<string name="speed">Bilis</string>
<string name="comics_archive_import_description">Maaari kang pumili ng isa o higit pang .cbz o .zip file, ang bawat file ay makikilala bilang isang hiwalay na manga.</string>
<string name="folder_with_images_import_description">Maaari kang pumili ng isang directory na may mga archive o mga larawan. Ang bawat archive (o subdirectory) ay makikilala bilang isang kabanata.</string>
<string name="find_similar">Maghanap ng katulad</string>
<string name="sync_auth_hint">Maaari kang mag-sign in sa isang umiiral na account o lumikha ng bago</string>
</resources>

View File

@@ -11,7 +11,7 @@
<string name="favourites">お気に入り</string>
<string name="error_occurred">エラーが発生しました</string>
<string name="details">詳細</string>
<string name="chapters">チャプター</string>
<string name="chapters"></string>
<string name="list">リスト</string>
<string name="detailed_list">詳細リスト</string>
<string name="grid">グリッド</string>
@@ -55,8 +55,8 @@
<string name="history_and_cache">履歴とキャッシュ</string>
<string name="clear_pages_cache">ページのキャッシュをクリアする</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="close_menu">閉じる</string>
<string name="open_menu">開く</string>
<string name="close_menu">メニューを閉じる</string>
<string name="open_menu">メニューを開く</string>
<string name="settings">設定</string>
<string name="light">ライトテーマ</string>
<string name="filter">フィルター</string>

View File

@@ -1,397 +1,397 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="favourites">Ulubione</string>
<string name="history">Historia</string>
<string name="error_occurred">Napotkano błąd</string>
<string name="details">Szczegółowy</string>
<string name="chapters">Rozdziały</string>
<string name="list">Lista</string>
<string name="detailed_list">Lista szczegółowa</string>
<string name="grid">Siatka</string>
<string name="list_mode">Tryb listy</string>
<string name="settings">Ustawienia</string>
<string name="loading_">Ładowanie…</string>
<string name="chapter_d_of_d">Rozdział %1$d z %2$d</string>
<string name="close">Zamknij</string>
<string name="clear_history">Wyczyść historię</string>
<string name="add">Dodaj</string>
<string name="save">Zapisz</string>
<string name="share">Udostępnij</string>
<string name="search">Szukaj</string>
<string name="search_manga">Szukaj mang</string>
<string name="manga_downloading_">Pobieranie…</string>
<string name="download_complete">Pobrano</string>
<string name="downloads">Pobrane</string>
<string name="by_name">Nazwa</string>
<string name="popular">Popularność</string>
<string name="newest">Najnowsze</string>
<string name="by_rating">Ocena</string>
<string name="filter">Filtry</string>
<string name="light">Jasny</string>
<string name="dark">Ciemny</string>
<string name="pages">Strony</string>
<string name="clear">Wyczyść</string>
<string name="remove">Usuń</string>
<string name="share_image">Udostępnij zdjęcie</string>
<string name="delete">Usuń</string>
<string name="no_description">Brak opisu</string>
<string name="read_mode">Tryb czytania</string>
<string name="network_error">Błąd sieci</string>
<string name="computing_">Obliczanie…</string>
<string name="try_again">Spróbuj ponownie</string>
<string name="nothing_found">Nic nie znaleziono</string>
<string name="history_is_empty">Brak historii</string>
<string name="read">Czytaj</string>
<string name="you_have_not_favourites_yet">Brak ulubionych</string>
<string name="add_to_favourites">Dodaj do ulubionych</string>
<string name="add_new_category">Nowa kategoria</string>
<string name="create_shortcut">Stwórz skrót</string>
<string name="share_s">Udostępnij %s</string>
<string name="processing_">Przetwarzanie…</string>
<string name="updated">Zaktualizowane</string>
<string name="_s_removed_from_history">„%s” usunięte z historii</string>
<string name="save_page">Zapisz stronę</string>
<string name="page_saved">Zapisano</string>
<string name="vibration">Wibracje</string>
<string name="manga_shelf">Biblioteka</string>
<string name="recent_manga">Ostatnie</string>
<string name="black_dark_theme">Tryb czarny</string>
<string name="preparing_">Przygotowywanie…</string>
<string name="file_not_found">Plik nieznaleziony</string>
<string name="yesterday">Wczoraj</string>
<string name="long_ago">Dawno temu</string>
<string name="group">Grupa</string>
<string name="today">Dzisiaj</string>
<string name="sign_in">Zaloguj</string>
<string name="next">Dalej</string>
<string name="confirm">Potwierdź</string>
<string name="welcome">Witaj</string>
<string name="state_finished">Skończone</string>
<string name="state_ongoing">W trakcie</string>
<string name="screenshots_allow">Zezwól</string>
<string name="suggestions">Proponowane</string>
<string name="suggestions_enable">Włącz propozycje</string>
<string name="enabled">Włączone</string>
<string name="disabled">Wyłączone</string>
<string name="never">Nigdy</string>
<string name="always">Zawsze</string>
<string name="search_chapters">Znajdź rozdział</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="appearance">Wygląd</string>
<string name="hide">Schowaj</string>
<string name="sync">Synchronizacja</string>
<string name="sync_title">Synchronizuj swoje dane</string>
<string name="name">Nazwa</string>
<string name="edit">Edytuj</string>
<string name="logout">Wyloguj</string>
<string name="undo">Cofnij</string>
<string name="send">Wyślij</string>
<string name="status_planned">Planowane</string>
<string name="status_reading">Czytane</string>
<string name="status_re_reading">Czytane ponownie</string>
<string name="status_completed">Skończone</string>
<string name="show_all">Pokaż wszystkie</string>
<string name="select_range">Wybierz zakres</string>
<string name="clear_all_history">Wyczyść całą historię</string>
<string name="last_2_hours">Ostatnie 2 godziny</string>
<string name="history_cleared">Historia wyczyszczona</string>
<string name="manage">Zarządzaj</string>
<string name="random">Losowe</string>
<string name="empty">Puste</string>
<string name="changelog">Lista zmian</string>
<string name="explore">Przeglądaj</string>
<string name="available">Dostępne</string>
<string name="options">Ustawienia</string>
<string name="source_disabled">Źródło wyłączone</string>
<string name="compact">Kompaktowy</string>
<string name="server_error">Błąd po stronie serwera (%1$d). Sprónuj ponownie później</string>
<string name="network_unavailable">Sieć niedostępna</string>
<string name="different_languages">Inne języki</string>
<string name="discard">Odrzuć</string>
<string name="brightness">Jasność</string>
<string name="contrast">Kontrast</string>
<string name="color_correction">Korekcja kolorów</string>
<string name="seconds_pattern">%ss</string>
<string name="off_short">Wyłącz</string>
<string name="automatic_scroll">Automatyczne przewijanie</string>
<string name="no_chapters">Brak rozdziałów</string>
<string name="incognito_mode">Tryb incognito</string>
<string name="downloading_manga">Pobieranie mangi</string>
<string name="removed_from_favourites">Usunięto z ulubionych</string>
<string name="enter_email_text">Wprowadź swój email aby kontynuować</string>
<string name="storage_usage">Wykorzystana pamięć</string>
<string name="saved_manga">Zapisane mangi</string>
<string name="no_bookmarks_yet">Brak zakładek</string>
<string name="no_bookmarks_summary">Możesz tworzyć zakładki w trakcie czytania mangi</string>
<string name="bookmarks_removed">Zakładki usunięte</string>
<string name="appwidget_recent_description">Twoje ostatnio czytane mangi</string>
<string name="disable_all">Wyłącz wszystkie</string>
<string name="disable_battery_optimization">Wyłącz optymalizację baterii</string>
<string name="detect_reader_mode">Autowykrywanie trybu czytania</string>
<string name="removed_from_history">Usunięte z historii</string>
<string name="bookmark_added">Dodano zakładkę</string>
<string name="bookmark_removed">Usunięto zakładkę</string>
<string name="bookmarks">Zakładki</string>
<string name="bookmark_remove">Usuń zakładkę</string>
<string name="bookmark_add">Dodaj zakładkę</string>
<string name="empty_favourite_categories">Brak ulubionych kategorii</string>
<string name="edit_category">Edytuj kategorię</string>
<string name="notifications_enable">Włącz powiadomienia</string>
<string name="back">Wróć</string>
<string name="account_already_exists">Konto już istnieje</string>
<string name="canceled">Anulowano</string>
<string name="download_slowdown">Zwolnienie pobierania</string>
<string name="chapters_empty">Brak rozdziałów w tej mandze</string>
<string name="various_languages">Różne języki</string>
<string name="only_using_wifi">Tylko na Wi-Fi</string>
<string name="screenshots_block_all">Zawsze blokuj</string>
<string name="date_format">Format daty</string>
<string name="genres">Gatunki</string>
<string name="find_genre">Znajdź gatunek</string>
<string name="read_more">Czytaj więcej</string>
<string name="other">Inne</string>
<string name="captcha_solve">Rozwiąż</string>
<string name="captcha_required">Wymagane CAPTCHA</string>
<string name="silent">Cichy</string>
<string name="tap_to_try_again">Dotknij aby spróbować ponownie</string>
<string name="just_now">Teraz</string>
<string name="data_restored">Przywrócone</string>
<string name="zoom_mode_fit_width">Dopasuj do szerokości</string>
<string name="zoom_mode_fit_height">Dopasuj do wysokości</string>
<string name="zoom_mode_fit_center">Dopasuj do środka</string>
<string name="create_category">Nowa kategoria</string>
<string name="no_update_available">Brak nowych aktualizacji</string>
<string name="check_for_updates">Sprawdź dostępność aktualizacji</string>
<string name="checking_for_updates">Sprawdzanie aktualizacji…</string>
<string name="app_version">Wersja %s</string>
<string name="about">O aplikacji</string>
<string name="categories_">Kategorie…</string>
<string name="rename">Zmień nazwę</string>
<string name="remove_category">Usuń</string>
<string name="text_empty_holder_primary">Jest tu dosyć pusto…</string>
<string name="favourites_categories">Ulubione kategorie</string>
<string name="light_indicator">Powiadomienie LED</string>
<string name="new_chapters">Nowe rozdziały</string>
<string name="close_menu">Zamknij kartę</string>
<string name="open_menu">Otwórz kartę</string>
<string name="local_storage">Pamięć wewnętrzna</string>
<string name="text_shelf_holder_primary">Tutaj będą wyświetlane Twoje mangi</string>
<string name="text_shelf_holder_secondary">Znajdź materiały do czytania w zakładce „Przeglądaj”</string>
<string name="text_feed_holder">W tym miejscu pojawią się powiadomienia o nowych rozdziałach z mang które czytasz</string>
<string name="pages_cache">Strony w pamięci podręcznej</string>
<string name="pages_animation">Animacja przewracania strony</string>
<string name="other_cache">Inne rzeczy w pamięci podręcznej</string>
<string name="open_in_browser">Otwórz w przeglądarce</string>
<string name="show_pages_numbers">Numerowane strony</string>
<string name="notifications">Powiadomienia</string>
<string name="notification_sound">Dźwięk powiadomień</string>
<string name="notifications_settings">Ustawienia powiadomień</string>
<string name="remote_sources">Zewnętrzne źródła</string>
<string name="theme">Motyw</string>
<string name="automatic">Systemowy</string>
<string name="history_and_cache">Historia i pamięć podręczna</string>
<string name="clear_pages_cache">Wyczyść pamięć podręczną stron</string>
<string name="cache">Pamięć podręczna</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="grid_size">Wielkość siatki</string>
<string name="search_on_s">Szukaj na %s</string>
<string name="delete_manga">Usuń mangę</string>
<string name="_continue">Dalej</string>
<string name="dont_ask_again">Nie pytaj ponownie</string>
<string name="cancelling_">Anulowanie…</string>
<string name="error">Błąd</string>
<string name="search_history_cleared">Wyczyszczone</string>
<string name="internal_storage">Pamięć wewnętrzna</string>
<string name="external_storage">Pamięć zewnętrzna</string>
<string name="domain">Domena</string>
<string name="application_update">Sprawdź dostępność nowej wersji aplikacji</string>
<string name="app_update_available">Nowa wersja aplikacji jest dostępna</string>
<string name="show_notification_app_update">Pokaż powiadomienie gdy nowa wersja jest dostępna</string>
<string name="large_manga_save_confirm">Ta manga ma %s. Zapisać wszystko?</string>
<string name="save_manga">Zapisz</string>
<string name="download">Pobierz</string>
<string name="read_from_start">Czytaj od początku</string>
<string name="category_delete_confirm">Usunąć kategorię „%s” z Twoich ulubionych? Wszystkie mangi w niej będą z niej usunięte.</string>
<string name="text_categories_holder">Możesz użyć kategorii do organizowania swoich ulubionych. Kliknij «+» aby stworzyć kategorię</string>
<string name="text_local_holder_primary">Najpierw coś zapisz</string>
<string name="not_available">Niedostępne</string>
<string name="done">Zapisz</string>
<string name="all_favourites">Wszystkie ulubione</string>
<string name="favourites_category_empty">Pusta kategoria</string>
<string name="read_later">Czytaj później</string>
<string name="updates">Aktualizacje</string>
<string name="new_version_s">Nowa wersja: %s</string>
<string name="size_s">Wielkość: %s</string>
<string name="waiting_for_network">Czekanie na sieć…</string>
<string name="rotate_screen">Obróć ekran</string>
<string name="update">Odśwież</string>
<string name="track_sources">Szukaj aktualizacji</string>
<string name="dont_check">Nie sprawdzaj</string>
<string name="enter_password">Wprowadź hasło</string>
<string name="wrong_password">Złe hasło</string>
<string name="protect_application">Chroń aplikację</string>
<string name="protect_application_summary">Pytaj o hasło przy starcie Kotatsu</string>
<string name="repeat_password">Wprowadź ponownie hasło</string>
<string name="black_dark_theme_summary">Zużywa mniej prądu na ekranach AMOLED</string>
<string name="backup_restore">Kopia zapasowa i przywracanie</string>
<string name="create_backup">Utwórz kopię zapasową danych</string>
<string name="restore_backup">Przywróć z kopii zapasowej</string>
<string name="nsfw">18+</string>
<string name="enabled_d_of_d">%1$d na %2$d włączone</string>
<string name="enter_category_name">Wprowadź nazwę kategorii</string>
<string name="standard">Standardowy</string>
<string name="webtoon">Webtoon</string>
<string name="reader_settings">Ustawienia czytnika</string>
<string name="switch_pages">Zmiana strony</string>
<string name="volume_buttons">Przyciski głośności</string>
<string name="warning">Uwaga</string>
<string name="taps_on_edges">Dotknięcie krawędzi</string>
<string name="updates_feed_cleared">Wyczyszczone</string>
<string name="scale_mode">Tryb skalowania</string>
<string name="clear_cookies">Wyczyść ciasteczka</string>
<string name="cookies_cleared">Wszystkie ciasteczka wyczyszczone</string>
<string name="search_only_on_s">Szukaj tylko na %s</string>
<string name="about_app_translation_summary">Przetłumacz tą aplikację</string>
<string name="about_app_translation">Tłumaczenie</string>
<string name="error_empty_name">Musisz wpisać nazwę</string>
<string name="available_sources">Dostępne źródła</string>
<string name="dynamic_theme">Motyw dynamiczny</string>
<string name="gestures_only">Tylko gesty</string>
<string name="cannot_find_available_storage">Brak dostępnej pamięci</string>
<string name="other_storage">Inny</string>
<string name="search_results">Wyniki wyszukiwania</string>
<string name="related">Szukaj podobnych</string>
<string name="data_restored_success">Wszystkie dane zostały przywrócone</string>
<string name="data_restored_with_errors">Dane zostały przywrócone, ale z błędami</string>
<string name="reverse">Od tyłu</string>
<string name="text_downloads_holder">Brak aktywnych pobrań</string>
<string name="system_default">Domyślny</string>
<string name="screenshots_policy">Polityka zrzutów ekranu</string>
<string name="suggestions_excluded_genres">Wyklucz gatunki</string>
<string name="suggestions_excluded_genres_summary">Określ gatunki, których nie chcesz widzieć w sugestiach</string>
<string name="logged_in_as">Zalogowano jako %s</string>
<string name="onboard_text">Wybierz języki, w których chcesz czytać mangi. Możesz zmienić to później w ustawieniach.</string>
<string name="report">Zgłoś</string>
<string name="data_deletion">Usuwanie danych</string>
<string name="invalid_domain_message">Nieważna domena</string>
<string name="reorder">Zmień kolejność</string>
<string name="exit_confirmation">Potwierdzenie wyjścia</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="reader_info_pattern">Rozdz. %1$d/%2$d Str. %3$d/%4$d</string>
<string name="network_unavailable_hint">Włącz Wi-Fi lub sieć komórkową, aby czytać mangę online</string>
<string name="_import">Importuj</string>
<string name="text_file_not_supported">Wybierz plik ZIP lub CBZ.</string>
<string name="restart">Uruchom ponownie</string>
<string name="clear_search_history">Wyczyść historię wyszukiwania</string>
<string name="operation_not_supported">Ta operacja nie jest obsługiwana</string>
<string name="wait_for_loading_finish">Poczekaj na zakończenie ładowania…</string>
<string name="sort_order">Tryb sortowania</string>
<string name="content">Treści</string>
<string name="filter_load_error">Nie można załadować listy gatunków</string>
<string name="status_on_hold">Wstrzymane</string>
<string name="status_dropped">Porzucone</string>
<string name="use_fingerprint">Użyj odcisku palca, jeśli jest dostępny</string>
<string name="appwidget_shelf_description">Mangi z Twoich ulubionych</string>
<string name="show_reading_indicators">Pokaż wskaźniki postępu czytania</string>
<string name="show_reading_indicators_summary">Pokaż procent przeczytania w historii i ulubionych</string>
<string name="exclude_nsfw_from_history_summary">Manga oznaczona jako NSFW nigdy nie zostanie dodana do historii, a Twoje postępy nie zostaną zapisane</string>
<string name="dns_over_https">DNS przez HTTPS</string>
<string name="default_mode">Tryb domyślny</string>
<string name="text_clear_history_prompt">Trwale wyczyścić całą historię czytania?</string>
<string name="_s_deleted_from_local_storage">„%s” usunięte z pamięci lokalnej</string>
<string name="clear_updates_feed">Wyczyść tablicę aktualizacji</string>
<string name="feed">Tablica</string>
<string name="text_delete_local_manga">Usunąć trwale „%s” z urządzenia?</string>
<string name="network_consumption_warning">Może to spowodować przeniesienie dużej ilości danych</string>
<string name="clear_thumbs_cache">Wyczyść pamięć podręczną miniatur</string>
<string name="text_search_holder_secondary">Spróbuj przeformułować zapytanie.</string>
<string name="text_history_holder_primary">To co czytasz będzie wyświetlane tutaj</string>
<string name="text_history_holder_secondary">Znajdź to, co warto przeczytać, w menu bocznym.</string>
<string name="text_local_holder_secondary">Zapisz ze źródeł online lub zaimportuj pliki.</string>
<string name="manga_save_location">Folder pobranych</string>
<string name="feed_will_update_soon">Aktualizacja tablicy rozpocznie się wkrótce</string>
<string name="passwords_mismatch">Niezgodne hasła</string>
<string name="update_check_failed">Nie można wyszukać aktualizacji</string>
<string name="right_to_left">Od prawej do lewej</string>
<string name="zoom_mode_keep_start">Trzymaj na starcie</string>
<string name="report_github">Utwórz problem na GitHubie</string>
<string name="backup_information">Możesz utworzyć kopię zapasową swojej historii i ulubionych oraz przywrócić ją</string>
<string name="reader_mode_hint">Wybrana konfiguracja zostanie zapamiętana dla tej mangi</string>
<string name="chapters_checking_progress">Sprawdzanie nowych rozdziałów: %1$d z %2$d</string>
<string name="clear_feed">Wyczyść tablicę</string>
<string name="text_clear_updates_feed_prompt">Wyczyścić trwale całą historię aktualizacji?</string>
<string name="check_for_new_chapters">Szukanie nowych rozdziałów</string>
<string name="auth_required">Zaloguj się, aby wyświetlić tę zawartość</string>
<string name="default_s">Domyślnie: %s</string>
<string name="_and_x_more">…i jeszcze %1$d</string>
<string name="protect_application_subtitle">Wprowadź hasło, aby uruchomić aplikację</string>
<string name="password_length_hint">Hasło musi mieć co najmniej 4 znaki</string>
<string name="text_clear_search_history_prompt">Trwale usunąć wszystkie ostatnie zapytania wyszukiwania?</string>
<string name="backup_saved">Zapisano kopię zapasową</string>
<string name="tracker_warning">Systemy niektórych urządzeń inaczej się zachowują. Może to zakłócać wykonywanie zadań w tle.</string>
<string name="queued">W kolejce</string>
<string name="chapter_is_missing_text">Pobierz lub przeczytaj ten brakujący rozdział online.</string>
<string name="chapter_is_missing">Brak rozdziału</string>
<string name="about_feedback">Komentarz</string>
<string name="about_feedback_4pda">Temat na 4PDA</string>
<string name="auth_complete">Uprawniony</string>
<string name="auth_not_supported_by">Logowanie na %s nie jest obsługiwane</string>
<string name="text_clear_cookies_prompt">Zostaniesz wylogowany ze wszystkich źródeł</string>
<string name="exclude_nsfw_from_history">Wyklucz mangi NSFW z historii</string>
<string name="enabled_sources">Wykorzystane źródła</string>
<string name="dynamic_theme_summary">Stosuje motyw utworzony na podstawie schematu kolorów Twojej tapety</string>
<string name="importing_progress">Importowanie mangi: %1$d z %2$d</string>
<string name="screenshots_block_nsfw">Zablokuj na NSFW</string>
<string name="suggestions_summary">Proponuj mangi na podstawie Twoich preferencji</string>
<string name="suggestions_info">Wszystkie dane są analizowane lokalnie na tym urządzeniu. Twoje dane osobowe nie są przekazywane do żadnych usług</string>
<string name="text_suggestion_holder">Zacznij czytać mangę, a otrzymasz spersonalizowane sugestie</string>
<string name="exclude_nsfw_from_suggestions">Nie proponuj mang NSFW</string>
<string name="reset_filter">Zresetuj filtr</string>
<string name="preload_pages">Ładuj wstępnie strony</string>
<string name="suggestions_updating">Aktualizowanie sugestii</string>
<string name="text_delete_local_manga_batch">Trwale usunąć wybrane elementy z urządzenia?</string>
<string name="removal_completed">Usuwanie zakończone</string>
<string name="batch_manga_save_confirm">Pobrać wszystkie wybrane mangi i ich rozdziały? Może to zużyć dużo danych i pamięci.</string>
<string name="parallel_downloads">Pobieranie równoległe</string>
<string name="download_slowdown_summary">Pomaga uniknąć blokowania Twojego adresu IP</string>
<string name="local_manga_processing">Przetwarzanie zapisanej mangi</string>
<string name="chapters_will_removed_background">Rozdziały zostaną usunięte w tle. Może to zająć trochę czasu</string>
<string name="email_enter_hint">Wpisz swój adres e-mail, aby kontynuować</string>
<string name="new_sources_text">Dostępne są nowe źródła mang</string>
<string name="check_new_chapters_title">Sprawdzaj dostępność nowych rozdziałów i informuj o nich</string>
<string name="show_notification_new_chapters_on">Będziesz otrzymywać powiadomienia o aktualizacjach mang, które czytasz</string>
<string name="show_notification_new_chapters_off">Nie będziesz otrzymywać powiadomień, ale nowe rozdziały będą podświetlane na listach</string>
<string name="tracking">Śledzenie</string>
<string name="detect_reader_mode_summary">Automatycznie wykryj, czy manga to webtoon</string>
<string name="disable_battery_optimization_summary">Pomaga w sprawdzaniu aktualizacji w tle</string>
<string name="crash_text">Coś poszło nie tak. Zgłoś błąd programistom, aby pomóc nam go naprawić.</string>
<string name="clear_cookies_summary">Może pomóc w przypadku niektórych problemów. Wszystkie autoryzacje zostaną unieważnione</string>
<string name="no_manga_sources">Brak źródeł mang</string>
<string name="no_manga_sources_text">Włącz źródła mang do czytania mang online</string>
<string name="categories_delete_confirm">Czy na pewno chcesz usunąć wybrane ulubione kategorie? Wszystkie w nich mangi zostaną usunięte i nie będzie można tego cofnąć.</string>
<string name="confirm_exit">Naciśnij ponownie Wstecz, aby wyjść</string>
<string name="exit_confirmation_summary">Naciśnij dwukrotnie przycisk Wstecz, aby wyjść z aplikacji</string>
<string name="removed_from_s">Usunięto z „%s”</string>
<string name="not_found_404">Treść nie została znaleziona lub została usunięta</string>
<string name="app_update_available_s">Dostępna aktualizacja aplikacji: %s</string>
<string name="reader_info_bar">Pokaż pasek informacji w czytniku</string>
<string name="comics_archive">Archiwum komiksów</string>
<string name="folder_with_images">Folder z obrazami</string>
<string name="importing_manga">Importowanie mangi</string>
<string name="import_completed">Importowanie zakończone</string>
<string name="import_completed_hint">Możesz usunąć oryginalny plik z pamięci, aby zaoszczędzić miejsce</string>
<string name="import_will_start_soon">Import rozpocznie się wkrótce</string>
<string name="color_correction_hint">Wybrane ustawienia kolorów zostaną zapamiętane dla tej mangi</string>
<string name="history_shortcuts">Pokaż ostatnie skróty do mang</string>
<string name="history_shortcuts_summary">Pokaż ostatnie mangi po długim naciśnięciu ikony aplikacji</string>
<string name="reader_control_ltr_summary">Stuknięcie w prawą krawędź lub naciśnięcie prawego klawisza zawsze powoduje przejście do następnej strony</string>
<string name="reader_control_ltr">Ergonomiczne sterowanie czytnikiem</string>
<string name="text_unsaved_changes_prompt">Zapisać czy odrzucić niezapisane zmiany?</string>
<string name="error_no_space_left">Brak miejsca w urządzeniu</string>
<string name="reader_slider">Pokaż suwak przełączania stron</string>
<string name="webtoon_zoom">Powiększanie webtoon</string>
<string name="webtoon_zoom_summary">Zezwalaj na gest powiększania/pomniejszania w trybie webtoon (beta)</string>
<string name="clear_new_chapters_counters">Wyczyść też informacje o nowych rozdziałach</string>
<string name="reset">Resetuj</string>
<string name="manga_error_description_pattern">Szczegóły błędu:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Spróbuj &lt;a href=%2$s&gt;otworzyć mangę w przeglądarce internetowej&lt;/a&gt; aby upewnić się, że jest dostępna w źródle&lt;br&gt;2. Jeśli jest dostępna, wyślij raport o błędzie do programistów.</string>
</resources>
<string name="favourites">Ulubione</string>
<string name="history">Historia</string>
<string name="error_occurred">Napotkano błąd</string>
<string name="details">Szczegółowy</string>
<string name="chapters">Rozdziały</string>
<string name="list">Lista</string>
<string name="detailed_list">Lista szczegółowa</string>
<string name="grid">Siatka</string>
<string name="list_mode">Tryb listy</string>
<string name="settings">Ustawienia</string>
<string name="loading_">Ładowanie…</string>
<string name="chapter_d_of_d">Rozdział %1$d z %2$d</string>
<string name="close">Zamknij</string>
<string name="clear_history">Wyczyść historię</string>
<string name="add">Dodaj</string>
<string name="save">Zapisz</string>
<string name="share">Udostępnij</string>
<string name="search">Szukaj</string>
<string name="search_manga">Szukaj mang</string>
<string name="manga_downloading_">Pobieranie…</string>
<string name="download_complete">Pobrano</string>
<string name="downloads">Pobrane</string>
<string name="by_name">Nazwa</string>
<string name="popular">Popularność</string>
<string name="newest">Najnowsze</string>
<string name="by_rating">Ocena</string>
<string name="filter">Filtry</string>
<string name="light">Jasny</string>
<string name="dark">Ciemny</string>
<string name="pages">Strony</string>
<string name="clear">Wyczyść</string>
<string name="remove">Usuń</string>
<string name="share_image">Udostępnij zdjęcie</string>
<string name="delete">Usuń</string>
<string name="no_description">Brak opisu</string>
<string name="read_mode">Tryb czytania</string>
<string name="network_error">Błąd sieci</string>
<string name="computing_">Obliczanie…</string>
<string name="try_again">Spróbuj ponownie</string>
<string name="nothing_found">Nic nie znaleziono</string>
<string name="history_is_empty">Brak historii</string>
<string name="read">Czytaj</string>
<string name="you_have_not_favourites_yet">Brak ulubionych</string>
<string name="add_to_favourites">Dodaj do ulubionych</string>
<string name="add_new_category">Nowa kategoria</string>
<string name="create_shortcut">Stwórz skrót</string>
<string name="share_s">Udostępnij %s</string>
<string name="processing_">Przetwarzanie…</string>
<string name="updated">Zaktualizowane</string>
<string name="_s_removed_from_history">„%s” usunięte z historii</string>
<string name="save_page">Zapisz stronę</string>
<string name="page_saved">Zapisano</string>
<string name="vibration">Wibracje</string>
<string name="manga_shelf">Biblioteka</string>
<string name="recent_manga">Ostatnie</string>
<string name="black_dark_theme">Tryb czarny</string>
<string name="preparing_">Przygotowywanie…</string>
<string name="file_not_found">Plik nieznaleziony</string>
<string name="yesterday">Wczoraj</string>
<string name="long_ago">Dawno temu</string>
<string name="group">Grupa</string>
<string name="today">Dzisiaj</string>
<string name="sign_in">Zaloguj</string>
<string name="next">Dalej</string>
<string name="confirm">Potwierdź</string>
<string name="welcome">Witaj</string>
<string name="state_finished">Skończone</string>
<string name="state_ongoing">W trakcie</string>
<string name="screenshots_allow">Zezwól</string>
<string name="suggestions">Proponowane</string>
<string name="suggestions_enable">Włącz propozycje</string>
<string name="enabled">Włączone</string>
<string name="disabled">Wyłączone</string>
<string name="never">Nigdy</string>
<string name="always">Zawsze</string>
<string name="search_chapters">Znajdź rozdział</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="appearance">Wygląd</string>
<string name="hide">Schowaj</string>
<string name="sync">Synchronizacja</string>
<string name="sync_title">Synchronizuj swoje dane</string>
<string name="name">Nazwa</string>
<string name="edit">Edytuj</string>
<string name="logout">Wyloguj</string>
<string name="undo">Cofnij</string>
<string name="send">Wyślij</string>
<string name="status_planned">Planowane</string>
<string name="status_reading">Czytane</string>
<string name="status_re_reading">Czytane ponownie</string>
<string name="status_completed">Skończone</string>
<string name="show_all">Pokaż wszystkie</string>
<string name="select_range">Wybierz zakres</string>
<string name="clear_all_history">Wyczyść całą historię</string>
<string name="last_2_hours">Ostatnie 2 godziny</string>
<string name="history_cleared">Historia wyczyszczona</string>
<string name="manage">Zarządzaj</string>
<string name="random">Losowe</string>
<string name="empty">Puste</string>
<string name="changelog">Lista zmian</string>
<string name="explore">Przeglądaj</string>
<string name="available">Dostępne</string>
<string name="options">Ustawienia</string>
<string name="source_disabled">Źródło wyłączone</string>
<string name="compact">Kompaktowy</string>
<string name="server_error">Błąd po stronie serwera (%1$d). Sprónuj ponownie później</string>
<string name="network_unavailable">Sieć niedostępna</string>
<string name="different_languages">Inne języki</string>
<string name="discard">Odrzuć</string>
<string name="brightness">Jasność</string>
<string name="contrast">Kontrast</string>
<string name="color_correction">Korekcja kolorów</string>
<string name="seconds_pattern">%ss</string>
<string name="off_short">Wyłącz</string>
<string name="automatic_scroll">Automatyczne przewijanie</string>
<string name="no_chapters">Brak rozdziałów</string>
<string name="incognito_mode">Tryb incognito</string>
<string name="downloading_manga">Pobieranie mangi</string>
<string name="removed_from_favourites">Usunięto z ulubionych</string>
<string name="enter_email_text">Wprowadź swój email aby kontynuować</string>
<string name="storage_usage">Wykorzystana pamięć</string>
<string name="saved_manga">Zapisane mangi</string>
<string name="no_bookmarks_yet">Brak zakładek</string>
<string name="no_bookmarks_summary">Możesz tworzyć zakładki w trakcie czytania mangi</string>
<string name="bookmarks_removed">Zakładki usunięte</string>
<string name="appwidget_recent_description">Twoje ostatnio czytane mangi</string>
<string name="disable_all">Wyłącz wszystkie</string>
<string name="disable_battery_optimization">Wyłącz optymalizację baterii</string>
<string name="detect_reader_mode">Autowykrywanie trybu czytania</string>
<string name="removed_from_history">Usunięte z historii</string>
<string name="bookmark_added">Dodano zakładkę</string>
<string name="bookmark_removed">Usunięto zakładkę</string>
<string name="bookmarks">Zakładki</string>
<string name="bookmark_remove">Usuń zakładkę</string>
<string name="bookmark_add">Dodaj zakładkę</string>
<string name="empty_favourite_categories">Brak ulubionych kategorii</string>
<string name="edit_category">Edytuj kategorię</string>
<string name="notifications_enable">Włącz powiadomienia</string>
<string name="back">Wróć</string>
<string name="account_already_exists">Konto już istnieje</string>
<string name="canceled">Anulowano</string>
<string name="download_slowdown">Zwolnienie pobierania</string>
<string name="chapters_empty">Brak rozdziałów w tej mandze</string>
<string name="various_languages">Różne języki</string>
<string name="only_using_wifi">Tylko na Wi-Fi</string>
<string name="screenshots_block_all">Zawsze blokuj</string>
<string name="date_format">Format daty</string>
<string name="genres">Gatunki</string>
<string name="find_genre">Znajdź gatunek</string>
<string name="read_more">Czytaj więcej</string>
<string name="other">Inne</string>
<string name="captcha_solve">Rozwiąż</string>
<string name="captcha_required">Wymagane CAPTCHA</string>
<string name="silent">Cichy</string>
<string name="tap_to_try_again">Dotknij aby spróbować ponownie</string>
<string name="just_now">Teraz</string>
<string name="data_restored">Przywrócone</string>
<string name="zoom_mode_fit_width">Dopasuj do szerokości</string>
<string name="zoom_mode_fit_height">Dopasuj do wysokości</string>
<string name="zoom_mode_fit_center">Dopasuj do środka</string>
<string name="create_category">Nowa kategoria</string>
<string name="no_update_available">Brak nowych aktualizacji</string>
<string name="check_for_updates">Sprawdź dostępność aktualizacji</string>
<string name="checking_for_updates">Sprawdzanie aktualizacji…</string>
<string name="app_version">Wersja %s</string>
<string name="about">O aplikacji</string>
<string name="categories_">Kategorie…</string>
<string name="rename">Zmień nazwę</string>
<string name="remove_category">Usuń</string>
<string name="text_empty_holder_primary">Jest tu dosyć pusto…</string>
<string name="favourites_categories">Ulubione kategorie</string>
<string name="light_indicator">Powiadomienie LED</string>
<string name="new_chapters">Nowe rozdziały</string>
<string name="close_menu">Zamknij kartę</string>
<string name="open_menu">Otwórz kartę</string>
<string name="local_storage">Pamięć wewnętrzna</string>
<string name="text_shelf_holder_primary">Tutaj będą wyświetlane Twoje mangi</string>
<string name="text_shelf_holder_secondary">Znajdź materiały do czytania w zakładce „Przeglądaj”</string>
<string name="text_feed_holder">W tym miejscu pojawią się powiadomienia o nowych rozdziałach z mang które czytasz</string>
<string name="pages_cache">Strony w pamięci podręcznej</string>
<string name="pages_animation">Animacja przewracania strony</string>
<string name="other_cache">Inne rzeczy w pamięci podręcznej</string>
<string name="open_in_browser">Otwórz w przeglądarce</string>
<string name="show_pages_numbers">Numerowane strony</string>
<string name="notifications">Powiadomienia</string>
<string name="notification_sound">Dźwięk powiadomień</string>
<string name="notifications_settings">Ustawienia powiadomień</string>
<string name="remote_sources">Zewnętrzne źródła</string>
<string name="theme">Motyw</string>
<string name="automatic">Systemowy</string>
<string name="history_and_cache">Historia i pamięć podręczna</string>
<string name="clear_pages_cache">Wyczyść pamięć podręczną stron</string>
<string name="cache">Pamięć podręczna</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="grid_size">Wielkość siatki</string>
<string name="search_on_s">Szukaj na %s</string>
<string name="delete_manga">Usuń mangę</string>
<string name="_continue">Dalej</string>
<string name="dont_ask_again">Nie pytaj ponownie</string>
<string name="cancelling_">Anulowanie…</string>
<string name="error">Błąd</string>
<string name="search_history_cleared">Wyczyszczone</string>
<string name="internal_storage">Pamięć wewnętrzna</string>
<string name="external_storage">Pamięć zewnętrzna</string>
<string name="domain">Domena</string>
<string name="application_update">Sprawdź dostępność nowej wersji aplikacji</string>
<string name="app_update_available">Nowa wersja aplikacji jest dostępna</string>
<string name="show_notification_app_update">Pokaż powiadomienie gdy nowa wersja jest dostępna</string>
<string name="large_manga_save_confirm">Ta manga ma %s. Zapisać wszystko?</string>
<string name="save_manga">Zapisz</string>
<string name="download">Pobierz</string>
<string name="read_from_start">Czytaj od początku</string>
<string name="category_delete_confirm">Usunąć kategorię „%s” z Twoich ulubionych? Wszystkie mangi w niej będą z niej usunięte.</string>
<string name="text_categories_holder">Możesz użyć kategorii do organizowania swoich ulubionych. Kliknij «+» aby stworzyć kategorię</string>
<string name="text_local_holder_primary">Najpierw coś zapisz</string>
<string name="not_available">Niedostępne</string>
<string name="done">Zapisz</string>
<string name="all_favourites">Wszystkie ulubione</string>
<string name="favourites_category_empty">Pusta kategoria</string>
<string name="read_later">Czytaj później</string>
<string name="updates">Aktualizacje</string>
<string name="new_version_s">Nowa wersja: %s</string>
<string name="size_s">Wielkość: %s</string>
<string name="waiting_for_network">Czekanie na sieć…</string>
<string name="rotate_screen">Obróć ekran</string>
<string name="update">Odśwież</string>
<string name="track_sources">Szukaj aktualizacji</string>
<string name="dont_check">Nie sprawdzaj</string>
<string name="enter_password">Wprowadź hasło</string>
<string name="wrong_password">Złe hasło</string>
<string name="protect_application">Chroń aplikację</string>
<string name="protect_application_summary">Pytaj o hasło przy starcie Kotatsu</string>
<string name="repeat_password">Wprowadź ponownie hasło</string>
<string name="black_dark_theme_summary">Zużywa mniej prądu na ekranach AMOLED</string>
<string name="backup_restore">Kopia zapasowa i przywracanie</string>
<string name="create_backup">Utwórz kopię zapasową danych</string>
<string name="restore_backup">Przywróć z kopii zapasowej</string>
<string name="nsfw">18+</string>
<string name="enabled_d_of_d">%1$d na %2$d włączone</string>
<string name="enter_category_name">Wprowadź nazwę kategorii</string>
<string name="standard">Standardowy</string>
<string name="webtoon">Webtoon</string>
<string name="reader_settings">Ustawienia czytnika</string>
<string name="switch_pages">Zmiana strony</string>
<string name="volume_buttons">Przyciski głośności</string>
<string name="warning">Uwaga</string>
<string name="taps_on_edges">Dotknięcie krawędzi</string>
<string name="updates_feed_cleared">Wyczyszczone</string>
<string name="scale_mode">Tryb skalowania</string>
<string name="clear_cookies">Wyczyść ciasteczka</string>
<string name="cookies_cleared">Wszystkie ciasteczka wyczyszczone</string>
<string name="search_only_on_s">Szukaj tylko na %s</string>
<string name="about_app_translation_summary">Przetłumacz tą aplikację</string>
<string name="about_app_translation">Tłumaczenie</string>
<string name="error_empty_name">Musisz wpisać nazwę</string>
<string name="available_sources">Dostępne źródła</string>
<string name="dynamic_theme">Motyw dynamiczny</string>
<string name="gestures_only">Tylko gesty</string>
<string name="cannot_find_available_storage">Brak dostępnej pamięci</string>
<string name="other_storage">Inny</string>
<string name="search_results">Wyniki wyszukiwania</string>
<string name="related">Szukaj podobnych</string>
<string name="data_restored_success">Wszystkie dane zostały przywrócone</string>
<string name="data_restored_with_errors">Dane zostały przywrócone, ale z błędami</string>
<string name="reverse">Od tyłu</string>
<string name="text_downloads_holder">Brak aktywnych pobrań</string>
<string name="system_default">Domyślny</string>
<string name="screenshots_policy">Polityka zrzutów ekranu</string>
<string name="suggestions_excluded_genres">Wyklucz gatunki</string>
<string name="suggestions_excluded_genres_summary">Określ gatunki, których nie chcesz widzieć w sugestiach</string>
<string name="logged_in_as">Zalogowano jako %s</string>
<string name="onboard_text">Wybierz języki, w których chcesz czytać mangi. Możesz zmienić to później w ustawieniach.</string>
<string name="report">Zgłoś</string>
<string name="data_deletion">Usuwanie danych</string>
<string name="invalid_domain_message">Nieważna domena</string>
<string name="reorder">Zmień kolejność</string>
<string name="exit_confirmation">Potwierdzenie wyjścia</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="reader_info_pattern">Rozdz. %1$d/%2$d Str. %3$d/%4$d</string>
<string name="network_unavailable_hint">Włącz Wi-Fi lub sieć komórkową, aby czytać mangę online</string>
<string name="_import">Importuj</string>
<string name="text_file_not_supported">Wybierz plik ZIP lub CBZ.</string>
<string name="restart">Uruchom ponownie</string>
<string name="clear_search_history">Wyczyść historię wyszukiwania</string>
<string name="operation_not_supported">Ta operacja nie jest obsługiwana</string>
<string name="wait_for_loading_finish">Poczekaj na zakończenie ładowania…</string>
<string name="sort_order">Tryb sortowania</string>
<string name="content">Treści</string>
<string name="filter_load_error">Nie można załadować listy gatunków</string>
<string name="status_on_hold">Wstrzymane</string>
<string name="status_dropped">Porzucone</string>
<string name="use_fingerprint">Użyj odcisku palca, jeśli jest dostępny</string>
<string name="appwidget_shelf_description">Mangi z Twoich ulubionych</string>
<string name="show_reading_indicators">Pokaż wskaźniki postępu czytania</string>
<string name="show_reading_indicators_summary">Pokaż procent przeczytania w historii i ulubionych</string>
<string name="exclude_nsfw_from_history_summary">Manga oznaczona jako NSFW nigdy nie zostanie dodana do historii, a Twoje postępy nie zostaną zapisane</string>
<string name="dns_over_https">DNS przez HTTPS</string>
<string name="default_mode">Tryb domyślny</string>
<string name="text_clear_history_prompt">Trwale wyczyścić całą historię czytania?</string>
<string name="_s_deleted_from_local_storage">„%s” usunięte z pamięci lokalnej</string>
<string name="clear_updates_feed">Wyczyść tablicę aktualizacji</string>
<string name="feed">Tablica</string>
<string name="text_delete_local_manga">Usunąć trwale „%s” z urządzenia?</string>
<string name="network_consumption_warning">Może to spowodować przeniesienie dużej ilości danych</string>
<string name="clear_thumbs_cache">Wyczyść pamięć podręczną miniatur</string>
<string name="text_search_holder_secondary">Spróbuj przeformułować zapytanie.</string>
<string name="text_history_holder_primary">To co czytasz będzie wyświetlane tutaj</string>
<string name="text_history_holder_secondary">Znajdź to, co warto przeczytać, w menu bocznym.</string>
<string name="text_local_holder_secondary">Zapisz ze źródeł online lub zaimportuj pliki.</string>
<string name="manga_save_location">Folder pobranych</string>
<string name="feed_will_update_soon">Aktualizacja tablicy rozpocznie się wkrótce</string>
<string name="passwords_mismatch">Niezgodne hasła</string>
<string name="update_check_failed">Nie można wyszukać aktualizacji</string>
<string name="right_to_left">Od prawej do lewej</string>
<string name="zoom_mode_keep_start">Trzymaj na starcie</string>
<string name="report_github">Utwórz problem na GitHubie</string>
<string name="backup_information">Możesz utworzyć kopię zapasową swojej historii i ulubionych oraz przywrócić ją</string>
<string name="reader_mode_hint">Wybrana konfiguracja zostanie zapamiętana dla tej mangi</string>
<string name="chapters_checking_progress">Sprawdzanie nowych rozdziałów: %1$d z %2$d</string>
<string name="clear_feed">Wyczyść tablicę</string>
<string name="text_clear_updates_feed_prompt">Wyczyścić trwale całą historię aktualizacji?</string>
<string name="check_for_new_chapters">Szukanie nowych rozdziałów</string>
<string name="auth_required">Zaloguj się, aby wyświetlić tę zawartość</string>
<string name="default_s">Domyślnie: %s</string>
<string name="_and_x_more">…i jeszcze %1$d</string>
<string name="protect_application_subtitle">Wprowadź hasło, aby uruchomić aplikację</string>
<string name="password_length_hint">Hasło musi mieć co najmniej 4 znaki</string>
<string name="text_clear_search_history_prompt">Trwale usunąć wszystkie ostatnie zapytania wyszukiwania?</string>
<string name="backup_saved">Zapisano kopię zapasową</string>
<string name="tracker_warning">Systemy niektórych urządzeń inaczej się zachowują. Może to zakłócać wykonywanie zadań w tle.</string>
<string name="queued">W kolejce</string>
<string name="chapter_is_missing_text">Pobierz lub przeczytaj ten brakujący rozdział online.</string>
<string name="chapter_is_missing">Brak rozdziału</string>
<string name="about_feedback">Komentarz</string>
<string name="about_feedback_4pda">Temat na 4PDA</string>
<string name="auth_complete">Uprawniony</string>
<string name="auth_not_supported_by">Logowanie na %s nie jest obsługiwane</string>
<string name="text_clear_cookies_prompt">Zostaniesz wylogowany ze wszystkich źródeł</string>
<string name="exclude_nsfw_from_history">Wyklucz mangi NSFW z historii</string>
<string name="enabled_sources">Wykorzystane źródła</string>
<string name="dynamic_theme_summary">Stosuje motyw utworzony na podstawie schematu kolorów Twojej tapety</string>
<string name="importing_progress">Importowanie mangi: %1$d z %2$d</string>
<string name="screenshots_block_nsfw">Zablokuj na NSFW</string>
<string name="suggestions_summary">Proponuj mangi na podstawie Twoich preferencji</string>
<string name="suggestions_info">Wszystkie dane są analizowane lokalnie na tym urządzeniu. Twoje dane osobowe nie są przekazywane do żadnych usług</string>
<string name="text_suggestion_holder">Zacznij czytać mangę, a otrzymasz spersonalizowane sugestie</string>
<string name="exclude_nsfw_from_suggestions">Nie proponuj mang NSFW</string>
<string name="reset_filter">Zresetuj filtr</string>
<string name="preload_pages">Ładuj wstępnie strony</string>
<string name="suggestions_updating">Aktualizowanie sugestii</string>
<string name="text_delete_local_manga_batch">Trwale usunąć wybrane elementy z urządzenia?</string>
<string name="removal_completed">Usuwanie zakończone</string>
<string name="batch_manga_save_confirm">Pobrać wszystkie wybrane mangi i ich rozdziały? Może to zużyć dużo danych i pamięci.</string>
<string name="parallel_downloads">Pobieranie równoległe</string>
<string name="download_slowdown_summary">Pomaga uniknąć blokowania Twojego adresu IP</string>
<string name="local_manga_processing">Przetwarzanie zapisanej mangi</string>
<string name="chapters_will_removed_background">Rozdziały zostaną usunięte w tle. Może to zająć trochę czasu</string>
<string name="email_enter_hint">Wpisz swój adres e-mail, aby kontynuować</string>
<string name="new_sources_text">Dostępne są nowe źródła mang</string>
<string name="check_new_chapters_title">Sprawdzaj dostępność nowych rozdziałów i informuj o nich</string>
<string name="show_notification_new_chapters_on">Będziesz otrzymywać powiadomienia o aktualizacjach mang, które czytasz</string>
<string name="show_notification_new_chapters_off">Nie będziesz otrzymywać powiadomień, ale nowe rozdziały będą podświetlane na listach</string>
<string name="tracking">Śledzenie</string>
<string name="detect_reader_mode_summary">Automatycznie wykryj, czy manga to webtoon</string>
<string name="disable_battery_optimization_summary">Pomaga w sprawdzaniu aktualizacji w tle</string>
<string name="crash_text">Coś poszło nie tak. Zgłoś błąd programistom, aby pomóc nam go naprawić.</string>
<string name="clear_cookies_summary">Może pomóc w przypadku niektórych problemów. Wszystkie autoryzacje zostaną unieważnione</string>
<string name="no_manga_sources">Brak źródeł mang</string>
<string name="no_manga_sources_text">Włącz źródła mang do czytania mang online</string>
<string name="categories_delete_confirm">Czy na pewno chcesz usunąć wybrane ulubione kategorie? Wszystkie w nich mangi zostaną usunięte i nie będzie można tego cofnąć.</string>
<string name="confirm_exit">Naciśnij ponownie Wstecz, aby wyjść</string>
<string name="exit_confirmation_summary">Naciśnij dwukrotnie przycisk Wstecz, aby wyjść z aplikacji</string>
<string name="removed_from_s">Usunięto z „%s”</string>
<string name="not_found_404">Treść nie została znaleziona lub została usunięta</string>
<string name="app_update_available_s">Dostępna aktualizacja aplikacji: %s</string>
<string name="reader_info_bar">Pokaż pasek informacji w czytniku</string>
<string name="comics_archive">Archiwum komiksów</string>
<string name="folder_with_images">Folder z obrazami</string>
<string name="importing_manga">Importowanie mangi</string>
<string name="import_completed">Importowanie zakończone</string>
<string name="import_completed_hint">Możesz usunąć oryginalny plik z pamięci, aby zaoszczędzić miejsce</string>
<string name="import_will_start_soon">Import rozpocznie się wkrótce</string>
<string name="color_correction_hint">Wybrane ustawienia kolorów zostaną zapamiętane dla tej mangi</string>
<string name="history_shortcuts">Pokaż ostatnie skróty do mang</string>
<string name="history_shortcuts_summary">Pokaż ostatnie mangi po długim naciśnięciu ikony aplikacji</string>
<string name="reader_control_ltr_summary">Stuknięcie w prawą krawędź lub naciśnięcie prawego klawisza zawsze powoduje przejście do następnej strony</string>
<string name="reader_control_ltr">Ergonomiczne sterowanie czytnikiem</string>
<string name="text_unsaved_changes_prompt">Zapisać czy odrzucić niezapisane zmiany?</string>
<string name="error_no_space_left">Brak miejsca w urządzeniu</string>
<string name="reader_slider">Pokaż suwak przełączania stron</string>
<string name="webtoon_zoom">Powiększanie webtoon</string>
<string name="webtoon_zoom_summary">Zezwalaj na gest powiększania/pomniejszania w trybie webtoon (beta)</string>
<string name="clear_new_chapters_counters">Wyczyść też informacje o nowych rozdziałach</string>
<string name="reset">Resetuj</string>
<string name="manga_error_description_pattern">Szczegóły błędu:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Spróbuj &lt;a href=%2$s&gt;otworzyć mangę w przeglądarce internetowej&lt;/a&gt; aby upewnić się, że jest dostępna w źródle&lt;br&gt;2. Jeśli jest dostępna, wyślij raport o błędzie do programistów.</string>
</resources>

View File

@@ -428,4 +428,6 @@
<string name="restore_backup_description">Імпортуйте раніше створену резервну копію даних користувача</string>
<string name="show_on_shelf">Показати на полиці</string>
<string name="sources_reorder_tip">Натисніть і утримуйте елемент, щоб змінити його порядок</string>
<string name="sync_auth_hint">Ви можете увійти в існуючий обліковий запис або створити новий</string>
<string name="find_similar">Знайти схожі</string>
</resources>

View File

@@ -1,33 +0,0 @@
package org.koitharu.kotatsu.core.github
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.headersContentLength
import org.junit.Assert
import org.junit.Test
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.parsers.util.await
class AppUpdateRepositoryTest {
private val okHttpClient = OkHttpClient()
private val repository = AppUpdateRepository(okHttpClient)
@Test
fun getLatestVersion() = runTest {
val version = repository.getLatestVersion()
val versionId = VersionId(version.name)
val apkHead = okHttpClient.newCall(
Request.Builder()
.url(version.apkUrl)
.head()
.build(),
).await()
Assert.assertTrue(versionId <= VersionId(BuildConfig.VERSION_NAME))
Assert.assertTrue(apkHead.isSuccessful)
Assert.assertEquals(version.apkSize, apkHead.headersContentLength())
}
}

View File

@@ -1,7 +1,13 @@
package org.koitharu.kotatsu.utils
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
@@ -27,6 +33,7 @@ class CompositeMutexTest {
}
}
yield()
assertEquals(1, mutex.size)
mutex.unlock(1)
val tryLock = withTimeoutOrNull(1000) {
mutex.lock(1)
@@ -49,4 +56,4 @@ class CompositeMutexTest {
job.cancelAndJoin()
}
}
}
}