Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e9fb5aab | ||
|
|
5fc08d9ecb | ||
|
|
e7eb61e3e5 | ||
|
|
dae3982e67 | ||
|
|
45b2d2bebe | ||
|
|
77fa34835f | ||
|
|
cee68069d6 | ||
|
|
48afc8624b | ||
|
|
b9b41ed491 | ||
|
|
427272aac1 | ||
|
|
78f417ebe1 | ||
|
|
3fd6bec433 | ||
|
|
262e26a0cc | ||
|
|
1b64c2a330 | ||
|
|
5ea0ecbd12 | ||
|
|
f9a1d1617e | ||
|
|
10d8365fc1 |
@@ -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'
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 439 KiB |
|
Before Width: | Height: | Size: 495 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 791 KiB |
|
Before Width: | Height: | Size: 844 KiB |
|
Before Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 375 KiB |
|
Before Width: | Height: | Size: 398 KiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ import okio.IOException
|
||||
|
||||
class CloudFlareProtectedException(
|
||||
val url: String,
|
||||
val headers: Headers,
|
||||
@Transient val headers: Headers,
|
||||
) : IOException("Protected by CloudFlare")
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
class WrongPasswordException : SecurityException()
|
||||
class WrongPasswordException : IllegalArgumentException()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package org.koitharu.kotatsu.local
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class LocalStorageChanges
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class RemoteListViewModel @Inject constructor(
|
||||
e.printStackTraceDebug()
|
||||
listError.value = e
|
||||
if (!mangaList.value.isNullOrEmpty()) {
|
||||
errorEvent.postCall(e)
|
||||
errorEvent.emitCall(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class FeedViewModel @Inject constructor(
|
||||
if (clearCounters) {
|
||||
repository.clearCounters()
|
||||
}
|
||||
onFeedCleared.postCall(Unit)
|
||||
onFeedCleared.emitCall(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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:<br><tt>%1$s</tt><br><br>1. Spróbuj <a href=%2$s>otworzyć mangę w przeglądarce internetowej</a> aby upewnić się, że jest dostępna w źródle<br>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:<br><tt>%1$s</tt><br><br>1. Spróbuj <a href=%2$s>otworzyć mangę w przeglądarce internetowej</a> aby upewnić się, że jest dostępna w źródle<br>2. Jeśli jest dostępna, wyślij raport o błędzie do programistów.</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||