Compare commits

...

35 Commits
v6.1.6 ... v6.2

Author SHA1 Message Date
Koitharu
32133d3358 Bump version 2023-10-16 17:48:25 +03:00
Koitharu
366e4f0da8 Disable mirror switching by default 2023-10-16 12:53:21 +03:00
Koitharu
3ef033c700 Update parsers 2023-10-16 12:46:39 +03:00
Koitharu
bef8e4652f Update acra credentials 2023-10-16 12:46:39 +03:00
Koitharu
8bfdf07a2f Fixes 2023-10-16 12:46:38 +03:00
Allan Nordhøy
f3e597275b Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/nb_NO/
Translation: Kotatsu/plurals
2023-10-16 12:40:38 +03:00
Koitharu
11feaae216 Translated using Weblate (Russian)
Currently translated at 100.0% (497 of 497 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
InfinityDouki56
fe2c1f9634 Translated using Weblate (Filipino)
Currently translated at 89.1% (443 of 497 strings)

Translated using Weblate (Filipino)

Currently translated at 89.2% (441 of 494 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
plum7x
0c7c6dc48a Translated using Weblate (Chinese (Traditional))
Currently translated at 99.5% (492 of 494 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Bai
503652f024 Translated using Weblate (Turkish)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (494 of 494 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-10-16 12:40:38 +03:00
ngocanhtve
0c4adc67ea Translated using Weblate (Vietnamese)
Currently translated at 85.1% (417 of 490 strings)

Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
BlackSpectrum
c7f5ce30b5 Translated using Weblate (Hindi)
Currently translated at 26.5% (130 of 490 strings)

Added translation using Weblate (Gujarati)

Co-authored-by: BlackSpectrum <tittan5000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
return_null
59d538824f Translated using Weblate (Chinese (Simplified))
Currently translated at 99.1% (486 of 490 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Nayuki
de79f39d16 Translated using Weblate (Thai)
Currently translated at 71.2% (349 of 490 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Clxff H3r4ld0
9792da3a5c Translated using Weblate (Indonesian)
Currently translated at 98.3% (482 of 490 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Макар Разин
c2407e6e41 Translated using Weblate (Czech)
Currently translated at 90.1% (448 of 497 strings)

Translated using Weblate (Serbian)

Currently translated at 30.3% (151 of 497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (490 of 490 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (490 of 490 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (490 of 490 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
gallegonovato
7321eeaed9 Translated using Weblate (Spanish)
Currently translated at 100.0% (497 of 497 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (493 of 494 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (490 of 490 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Cookies
9876adf676 Translated using Weblate (Vietnamese)
Currently translated at 81.3% (398 of 489 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-10-16 12:40:38 +03:00
Koitharu
d29e979fbf Add option to order favorites by reading progress 2023-10-13 16:31:51 +03:00
Koitharu
35baf4b58d Add option to order history and favorites by new chapters 2023-10-13 16:26:30 +03:00
Koitharu
97524d66f2 Fix pages thumbnails loading 2023-10-13 15:58:05 +03:00
Koitharu
5b53f8c27d Improve list options configuring 2023-10-13 14:21:28 +03:00
Koitharu
d4588570e6 Add option to disable new sources tip 2023-10-12 13:34:35 +03:00
Koitharu
cc2f9d4529 Improve chapters mapping 2023-10-12 13:24:51 +03:00
Koitharu
3def71ccc1 Merge branch 'feature/32-bit' into devel 2023-10-12 13:03:58 +03:00
Koitharu
b313c64648 Apply color config on-the-fly 2023-10-12 12:18:27 +03:00
Koitharu
f7e7c84317 Apply color config on-the-fly 2023-10-12 11:31:28 +03:00
Koitharu
ee1c532d53 Update progress 2023-10-12 10:42:28 +03:00
Koitharu
6993cec85e Fix new chapters counter in details screen 2023-10-12 10:42:28 +03:00
Koitharu
0b19f56215 Optimize finding saved manga for remote one 2023-10-12 10:42:28 +03:00
Zakhar Timoshenko
817ce7e8df 32-bit colors mode implementing 2023-10-11 21:18:13 +03:00
Zakhar Timoshenko
2b2498cb38 UI tweaks 2023-10-11 19:23:51 +03:00
Koitharu
e4efd0f696 Refactor manga details loading 2023-10-10 11:56:23 +03:00
Koitharu
fbb267e11c Update parsers 2023-10-09 10:10:55 +03:00
Koitharu
5740af05fa Update dependencies 2023-10-06 09:44:53 +03:00
111 changed files with 1408 additions and 826 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 584
versionName = '6.1.6'
versionCode = 587
versionName = '6.2'
generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
@@ -81,7 +81,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:3d7e62d2fe') {
implementation('com.github.KotatsuApp:kotatsu-parsers:a61e441e79') {
exclude group: 'org.json', module: 'json'
}
@@ -90,7 +90,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.activity:activity-ktx:1.8.0-rc01'
implementation 'androidx.activity:activity-ktx:1.8.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
@@ -102,7 +102,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
// TODO https://issuetracker.google.com/issues/254846063
@@ -125,14 +125,14 @@ dependencies {
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.48'
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.4.0'
implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:169806d928'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:cf089a264d'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
@@ -155,6 +155,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.5.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48.1'
}

View File

@@ -2,9 +2,9 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.Date
@Parcelize
@@ -12,7 +12,7 @@ data class FavouriteCategory(
val id: Long,
val title: String,
val sortKey: Int,
val order: SortOrder,
val order: ListSortOrder,
val createdAt: Date,
val isTrackingEnabled: Boolean,
val isVisibleInLibrary: Boolean,

View File

@@ -43,5 +43,7 @@ class MangaIntent private constructor(
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
fun of(manga: Manga) = MangaIntent(manga, manga.id, null)
}
}

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.lang.ref.WeakReference
import java.util.*
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
@@ -50,7 +50,7 @@ class MangaLoaderContextImpl @Inject constructor(
}
override fun encodeBase64(data: ByteArray): String {
return Base64.encodeToString(data, Base64.NO_PADDING)
return Base64.encodeToString(data, Base64.NO_WRAP)
}
override fun decodeBase64(data: String): ByteArray {

View File

@@ -22,7 +22,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
@@ -72,6 +72,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
var historyListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) }
var suggestionsListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_SUGGESTIONS, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_SUGGESTIONS, value) }
var favoritesListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_FAVORITES, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_FAVORITES, value) }
var isNsfwContentDisabled: Boolean
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }
@@ -176,7 +188,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
val isMirrorSwitchingAvailable: Boolean
get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, true)
get() = prefs.getBoolean(KEY_MIRROR_SWITCHING, false)
val isExitConfirmationEnabled: Boolean
get() = prefs.getBoolean(KEY_EXIT_CONFIRM, false)
@@ -201,6 +213,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
val isNewSourcesTipEnabled: Boolean
get() = prefs.getBoolean(KEY_SOURCES_NEW, true)
val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
@@ -304,8 +319,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
var historySortOrder: HistoryOrder
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, HistoryOrder.UPDATED)
var historySortOrder: ListSortOrder
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, ListSortOrder.UPDATED)
set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) }
val isRelatedMangaEnabled: Boolean
@@ -336,6 +351,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
return policy.isNetworkAllowed(connectivityManager)
}
val is32BitColorsEnabled: Boolean
get() = prefs.getBoolean(KEY_32BIT_COLOR, false)
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -401,6 +419,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val TRACK_FAVOURITES = "favourites"
const val KEY_LIST_MODE = "list_mode_2"
const val KEY_LIST_MODE_HISTORY = "list_mode_history"
const val KEY_LIST_MODE_FAVORITES = "list_mode_favorites"
const val KEY_LIST_MODE_SUGGESTIONS = "list_mode_suggestions"
const val KEY_THEME = "theme"
const val KEY_COLOR_THEME = "color_theme"
const val KEY_THEME_AMOLED = "amoled_theme"
@@ -474,6 +495,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_LOGGING_ENABLED = "logging"
const val KEY_LOGS_SHARE = "logs_share"
const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_SOURCES_NEW = "sources_new"
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed"
const val KEY_SSL_BYPASS = "ssl_bypass"
@@ -491,6 +513,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
const val KEY_NAV_MAIN = "nav_main"
const val KEY_32BIT_COLOR = "enhanced_colors"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.coroutineContext
@Deprecated("", replaceWith = ReplaceWith("CompositeMutex2"))
class CompositeMutex<T : Any> : Set<T> {
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()

View File

@@ -15,6 +15,7 @@ class ViewBadge(
) : View.OnLayoutChangeListener, DefaultLifecycleObserver {
private var badgeDrawable: BadgeDrawable? = null
private var maxCharacterCount: Int = -1
var counter: Int
get() = badgeDrawable?.number ?: 0
@@ -48,8 +49,16 @@ class ViewBadge(
clearBadge()
}
fun setMaxCharacterCount(value: Int) {
maxCharacterCount = value
badgeDrawable?.maxCharacterCount = value
}
private fun initBadge(): BadgeDrawable {
val badge = BadgeDrawable.create(anchor.context)
if (maxCharacterCount > 0) {
badge.maxCharacterCount = maxCharacterCount
}
anchor.addOnLayoutChangeListener(this)
BadgeUtils.attachBadgeDrawable(badge, anchor)
badgeDrawable = badge

View File

@@ -55,3 +55,5 @@ inline fun <reified E : Enum<E>> Collection<E>.toEnumSet(): EnumSet<E> = if (isE
} else {
EnumSet.copyOf(this)
}
fun <E : Enum<E>> Collection<E>.sortedByOrdinal() = sortedBy { it.ordinal }

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -79,3 +80,11 @@ fun <T> Deferred<T>.getCompletionResultOrNull(): Result<T>? = if (isCompleted) {
} else {
null
}
fun <T> Deferred<T>.peek(): T? = if (isCompleted) {
runCatchingCancellable {
getCompleted()
}.getOrNull()
} else {
null
}

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.details.data
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.data.filterChapters
data class MangaDetails(
private val manga: Manga,
private val localManga: LocalManga?,
val description: CharSequence?,
val isLoaded: Boolean,
) {
val id: Long
get() = manga.id
val chapters: Map<String?, List<MangaChapter>> = manga.chapters?.groupBy { it.branch }.orEmpty()
val branches: Set<String?>
get() = chapters.keys
val allChapters: List<MangaChapter>
get() = manga.chapters.orEmpty()
val isLocal
get() = manga.isLocal
val local: LocalManga?
get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null
fun toManga() = manga
fun filterChapters(branch: String?) = MangaDetails(
manga = manga.filterChapters(branch),
localManga = localManga?.run {
copy(manga = manga.filterChapters(branch))
},
description = description,
isLoaded = isLoaded,
)
}

View File

@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
@@ -20,7 +20,7 @@ import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject
@Deprecated("")
/* TODO: remove */
class DetailsInteractor @Inject constructor(
private val historyRepository: HistoryRepository,
private val favouritesRepository: FavouritesRepository,
@@ -66,13 +66,22 @@ class DetailsInteractor @Inject constructor(
}
}
suspend fun updateLocal(subject: DoubleManga?, localManga: LocalManga): DoubleManga? {
return if (subject?.any?.id == localManga.manga.id) {
subject.copy(
localManga = runCatchingCancellable {
localMangaRepository.getDetails(localManga.manga)
},
)
suspend fun updateLocal(subject: MangaDetails?, localManga: LocalManga): MangaDetails? {
subject ?: return null
return if (subject.id == localManga.manga.id) {
if (subject.isLocal) {
subject.copy(
manga = localManga.manga,
)
} else {
subject.copy(
localManga = runCatchingCancellable {
localManga.copy(
manga = localMangaRepository.getDetails(localManga.manga),
)
}.getOrNull() ?: subject.local,
)
}
} else {
subject
}

View File

@@ -0,0 +1,85 @@
package org.koitharu.kotatsu.details.domain
import android.text.Html
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.getSpans
import androidx.core.text.parseAsHtml
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.peek
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.recoverNotNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
class DetailsLoadUseCase @Inject constructor(
private val mangaDataRepository: MangaDataRepository,
private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val recoverUseCase: RecoverMangaUseCase,
private val imageGetter: Html.ImageGetter,
) {
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
val manga = requireNotNull(mangaDataRepository.resolveIntent(intent)) {
"Cannot resolve intent $intent"
}
val local = if (!manga.isLocal) {
async {
localMangaRepository.findSavedManga(manga)
}
} else {
null
}
send(MangaDetails(manga, null, null, false))
val details = getDetails(manga)
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
}
private suspend fun getDetails(seed: Manga) = runCatchingCancellable {
val repository = mangaRepositoryFactory.create(seed.source)
repository.getDetails(seed)
}.recoverNotNull { e ->
if (e is NotFoundException) {
recoverUseCase(seed)
} else {
null
}
}.getOrThrow()
private suspend fun String.parseAsHtml(withImages: Boolean): CharSequence? {
return if (withImages) {
runInterruptible(Dispatchers.IO) {
parseAsHtml(imageGetter = imageGetter)
}.filterSpans()
} else {
runInterruptible(Dispatchers.Default) {
parseAsHtml()
}.filterSpans().sanitize()
}.takeUnless { it.isBlank() }
}
private fun Spanned.filterSpans(): Spanned {
val spannable = SpannableString.valueOf(this)
val spans = spannable.getSpans<ForegroundColorSpan>()
for (span in spans) {
spannable.removeSpan(span)
}
return spannable
}
}

View File

@@ -1,91 +0,0 @@
package org.koitharu.kotatsu.details.domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.explore.domain.RecoverMangaUseCase
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.recoverNotNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
class DoubleMangaLoadUseCase @Inject constructor(
private val mangaDataRepository: MangaDataRepository,
private val localMangaRepository: LocalMangaRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val recoverUseCase: RecoverMangaUseCase,
) {
operator fun invoke(manga: Manga): Flow<DoubleManga> = flow {
var lastValue: DoubleManga? = null
var emitted = false
invokeImpl(manga).collect {
lastValue = it
if (it.any != null) {
emitted = true
emit(it)
}
}
if (!emitted) {
lastValue?.requireAny()
}
}.flowOn(Dispatchers.Default)
operator fun invoke(mangaId: Long): Flow<DoubleManga> = flow {
emit(mangaDataRepository.findMangaById(mangaId) ?: throwNFE())
}.flatMapLatest { invoke(it) }
operator fun invoke(intent: MangaIntent): Flow<DoubleManga> = flow {
emit(mangaDataRepository.resolveIntent(intent) ?: throwNFE())
}.flatMapLatest { invoke(it) }
private suspend fun loadLocal(manga: Manga): Result<Manga>? {
return runCatchingCancellable {
if (manga.isLocal) {
localMangaRepository.getDetails(manga)
} else {
localMangaRepository.findSavedManga(manga)?.manga
} ?: return null
}
}
private suspend fun loadRemote(manga: Manga): Result<Manga>? {
return runCatchingCancellable {
val seed = if (manga.isLocal) {
localMangaRepository.getRemoteManga(manga)
} else {
manga
} ?: return null
val repository = mangaRepositoryFactory.create(seed.source)
repository.getDetails(seed)
}.recoverNotNull { e ->
if (e is NotFoundException) {
recoverUseCase(manga)
} else {
null
}
}
}
private fun invokeImpl(manga: Manga): Flow<DoubleManga> = combine(
flow { emit(null); emit(loadRemote(manga)) },
flow { emit(null); emit(loadLocal(manga)) },
) { remote, local ->
DoubleManga(
remoteManga = remote,
localManga = local,
)
}
private fun throwNFE(): Nothing = throw NotFoundException("Cannot find manga", "")
}

View File

@@ -0,0 +1,55 @@
package org.koitharu.kotatsu.details.domain
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.findChapter
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
class ProgressUpdateUseCase @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val database: MangaDatabase,
private val localMangaRepository: LocalMangaRepository,
) {
suspend operator fun invoke(manga: Manga): Float {
val history = database.historyDao.find(manga.id) ?: return PROGRESS_NONE
val seed = if (manga.isLocal) {
localMangaRepository.getRemoteManga(manga) ?: manga
} else {
manga
}
val repo = mangaRepositoryFactory.create(seed.source)
val details = if (manga.source != seed.source || seed.chapters.isNullOrEmpty()) {
repo.getDetails(seed)
} else {
seed
}
val chapter = details.findChapter(history.chapterId) ?: return PROGRESS_NONE
val chapters = details.getChapters(chapter.branch) ?: return PROGRESS_NONE
val chaptersCount = chapters.size
if (chaptersCount == 0) {
return PROGRESS_NONE
}
val chapterIndex = chapters.indexOfFirst { x -> x.id == history.chapterId }
val pagesCount = repo.getPages(chapter).size
if (pagesCount == 0) {
return PROGRESS_NONE
}
val pagePercent = (history.page + 1) / pagesCount.toFloat()
val ppc = 1f / chaptersCount
val result = ppc * chapterIndex + ppc * pagePercent
if (result != history.percent) {
database.historyDao.update(
history.copy(
chapterId = chapter.id,
percent = result,
),
)
}
return result
}
}

View File

@@ -1,81 +0,0 @@
package org.koitharu.kotatsu.details.domain.model
import org.koitharu.kotatsu.core.model.findById
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.reader.data.filterChapters
data class DoubleManga(
private val remoteManga: Result<Manga>?,
private val localManga: Result<Manga>?,
) {
constructor(manga: Manga) : this(
remoteManga = if (manga.source != MangaSource.LOCAL) Result.success(manga) else null,
localManga = if (manga.source == MangaSource.LOCAL) Result.success(manga) else null,
)
val remote: Manga?
get() = remoteManga?.getOrNull()
val local: Manga?
get() = localManga?.getOrNull()
val any: Manga?
get() = remote ?: local
val hasRemote: Boolean
get() = remoteManga?.isSuccess == true
val hasLocal: Boolean
get() = localManga?.isSuccess == true
val chapters: List<MangaChapter>? by lazy(LazyThreadSafetyMode.PUBLICATION) {
mergeChapters()
}
fun hasChapter(id: Long): Boolean {
return local?.chapters?.findById(id) != null || remote?.chapters?.findById(id) != null
}
fun requireAny(): Manga {
val result = remoteManga?.getOrNull() ?: localManga?.getOrNull()
if (result != null) {
return result
}
throw (
remoteManga?.exceptionOrNull()
?: localManga?.exceptionOrNull()
?: IllegalStateException("No online either local manga available")
)
}
fun filterChapters(branch: String?) = DoubleManga(
remoteManga?.map { it.filterChapters(branch) },
localManga?.map { it.filterChapters(branch) },
)
private fun mergeChapters(): List<MangaChapter>? {
val remoteChapters = remote?.chapters
val localChapters = local?.chapters
if (localChapters == null && remoteChapters == null) {
return null
}
val localMap = if (!localChapters.isNullOrEmpty()) {
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
} else {
null
}
val result = ArrayList<MangaChapter>(maxOf(remoteChapters?.size ?: 0, localChapters?.size ?: 0))
remoteChapters?.forEach { r ->
localMap?.remove(r.id)?.let { l ->
result.add(l)
} ?: result.add(r)
}
localMap?.values?.let {
result.addAll(it)
}
return result
}
}

View File

@@ -2,33 +2,30 @@ package org.koitharu.kotatsu.details.ui
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
fun mapChapters(
remoteManga: Manga?,
localManga: Manga?,
fun MangaDetails.mapChapters(
history: MangaHistory?,
newCount: Int,
branch: String?,
bookmarks: List<Bookmark>,
): List<ChapterListItem> {
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
val localChapters = localManga?.getChapters(branch).orEmpty()
val remoteChapters = chapters[branch].orEmpty()
val localChapters = local?.manga?.getChapters(branch).orEmpty()
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
return emptyList()
}
val bookmarked = bookmarks.mapToSet { it.chapterId }
val currentId = history?.chapterId ?: 0L
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
val chaptersSize = maxOf(remoteChapters.size, localChapters.size)
val ids = buildSet(chaptersSize) {
val ids = buildSet(maxOf(remoteChapters.size, localChapters.size)) {
remoteChapters.mapTo(this) { it.id }
localChapters.mapTo(this) { it.id }
}
val result = ArrayList<ChapterListItem>(chaptersSize)
val result = ArrayList<ChapterListItem>(ids.size)
val localMap = if (localChapters.isNotEmpty()) {
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
} else {
@@ -40,7 +37,7 @@ fun mapChapters(
if (chapter.id == currentId) {
isUnread = true
}
result += chapter.toListItem(
result += (local ?: chapter).toListItem(
isCurrent = chapter.id == currentId,
isUnread = isUnread,
isNew = isUnread && result.size >= newFrom,
@@ -57,7 +54,7 @@ fun mapChapters(
isCurrent = chapter.id == currentId,
isUnread = isUnread,
isNew = false,
isDownloaded = remoteManga != null,
isDownloaded = !isLocal,
isBookmarked = chapter.id in bookmarked,
)
}

View File

@@ -39,7 +39,6 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor
@@ -75,7 +74,6 @@ class DetailsActivity :
@Inject
lateinit var appShortcutManager: AppShortcutManager
private lateinit var viewBadge: ViewBadge
private var buttonTip: WeakReference<ButtonTip>? = null
private val viewModel: DetailsViewModel by viewModels()
@@ -92,7 +90,6 @@ class DetailsActivity :
viewBinding.buttonRead.setOnLongClickListener(this)
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
viewBinding.buttonDropdown.setOnClickListener(this)
viewBadge = ViewBadge(viewBinding.buttonRead, this)
if (viewBinding.layoutBottom != null) {
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
@@ -113,7 +110,6 @@ class DetailsActivity :
onBackPressedDispatcher.addCallback(chaptersMenuProvider)
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
viewModel.onError.observeEvent(
this,
@@ -139,7 +135,7 @@ class DetailsActivity :
}
viewModel.isChaptersReversed.observe(
this,
MenuInvalidator(viewBinding.toolbarChapters ?: this)
MenuInvalidator(viewBinding.toolbarChapters ?: this),
)
viewModel.favouriteCategories.observe(this, MenuInvalidator(this))
viewModel.branches.observe(this) {
@@ -148,7 +144,7 @@ class DetailsActivity :
viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted.observeEvent(
this,
DownloadStartedObserver(viewBinding.containerDetails)
DownloadStartedObserver(viewBinding.containerDetails),
)
addMenuProvider(
@@ -255,7 +251,7 @@ class DetailsActivity :
window.setNavigationBarTransparentCompat(
this,
viewBinding.layoutBottom?.elevation ?: 0f,
0.9f
0.9f,
)
}
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
@@ -281,24 +277,20 @@ class DetailsActivity :
info.currentChapter >= 0 -> getString(
R.string.chapter_d_of_d,
info.currentChapter + 1,
info.totalChapters
info.totalChapters,
)
info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(
R.plurals.chapters,
info.totalChapters,
info.totalChapters
info.totalChapters,
)
}
viewBinding.toolbarChapters?.title = text
viewBinding.textViewTitle?.text = text
}
private fun onNewChaptersChanged(newChapters: Int) {
viewBadge.counter = newChapters
}
private fun showBranchPopupMenu(v: View) {
val menu = PopupMenu(v.context, v)
val branches = viewModel.branches.value
@@ -311,8 +303,8 @@ class DetailsActivity :
ForegroundColorSpan(
v.context.getThemeColor(
android.R.attr.textColorSecondary,
Color.LTGRAY
)
Color.LTGRAY,
),
),
RelativeSizeSpan(0.74f),
) {

View File

@@ -11,6 +11,8 @@ import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -21,6 +23,7 @@ import coil.request.SuccessResult
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
@@ -37,6 +40,7 @@ import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableTop
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.isTextTruncated
import org.koitharu.kotatsu.core.util.ext.observe
@@ -70,6 +74,7 @@ import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorShee
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class DetailsFragment :
@@ -113,9 +118,9 @@ class DetailsFragment :
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
viewModel.localSize.observe(viewLifecycleOwner, ::onLocalSizeChanged)
viewModel.relatedManga.observe(viewLifecycleOwner, ::onRelatedMangaChanged)
combine(viewModel.chapters, viewModel.newChaptersCount, ::Pair).observe(viewLifecycleOwner, ::onChaptersChanged)
}
override fun onItemClick(item: Bookmark, view: View) {
@@ -191,14 +196,28 @@ class DetailsFragment :
}
}
private fun onChaptersChanged(chapters: List<ChapterListItem>?) {
private fun onChaptersChanged(data: Pair<List<ChapterListItem>?, Int>) {
val (chapters, newChapters) = data
val infoLayout = requireViewBinding().infoLayout
if (chapters.isNullOrEmpty()) {
infoLayout.textViewChapters.isVisible = false
} else {
val count = chapters.countChaptersByBranch()
infoLayout.textViewChapters.isVisible = true
infoLayout.textViewChapters.text = resources.getQuantityString(R.plurals.chapters, count, count)
val chaptersText = resources.getQuantityString(R.plurals.chapters, count, count)
infoLayout.textViewChapters.text = if (newChapters == 0) {
chaptersText
} else {
buildSpannedString {
append(chaptersText)
append(' ')
color(infoLayout.textViewChapters.context.getThemeColor(materialR.attr.colorError)) {
append("(+")
append(newChapters.toString())
append(')')
}
}
}
}
}

View File

@@ -1,12 +1,5 @@
package org.koitharu.kotatsu.details.ui
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.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -17,22 +10,21 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
@@ -40,17 +32,15 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.details.domain.RelatedMangaUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.model.MangaBranch
@@ -74,22 +64,19 @@ class DetailsViewModel @Inject constructor(
private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val imageGetter: Html.ImageGetter,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val downloadScheduler: DownloadWorker.Scheduler,
private val interactor: DetailsInteractor,
savedStateHandle: SavedStateHandle,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
private val relatedMangaUseCase: RelatedMangaUseCase,
private val extraProvider: ListExtraProvider,
networkState: NetworkState,
private val detailsLoadUseCase: DetailsLoadUseCase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
private val mangaId = intent.mangaId
private val doubleManga: MutableStateFlow<DoubleManga?> =
MutableStateFlow(intent.manga?.let { DoubleManga(it) })
private var loadingJob: Job
val onShowToast = MutableEventFlow<Int>()
@@ -97,8 +84,9 @@ class DetailsViewModel @Inject constructor(
val onSelectChapter = MutableEventFlow<Long>()
val onDownloadStarted = MutableEventFlow<Unit>()
val manga = doubleManga.map { it?.any }
.stateIn(viewModelScope, SharingStarted.Eagerly, doubleManga.value?.any)
val details = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
val manga = details.map { x -> x?.toManga() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val history = historyRepository.observeOne(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
@@ -106,8 +94,13 @@ class DetailsViewModel @Inject constructor(
val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val newChaptersCount = interactor.observeNewChapters(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
val newChaptersCount = details.flatMapLatest { d ->
if (d?.isLocal == false) {
interactor.observeNewChapters(mangaId)
} else {
flowOf(0)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
private val chaptersQuery = MutableStateFlow("")
val selectedBranch = MutableStateFlow<String?>(null)
@@ -135,28 +128,17 @@ class DetailsViewModel @Inject constructor(
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val localSize = doubleManga
.map {
val local = it?.local
if (local != null) {
val file = local.url.toUri().toFileOrNull()
file?.computeSize() ?: 0L
} else {
0L
}
val localSize = details
.map { it?.local }
.distinctUntilChanged()
.map { local ->
local?.file?.computeSize() ?: 0L
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
val description = manga
.distinctUntilChangedBy { it?.description.orEmpty() }
.transformLatest {
val description = it?.description
if (description.isNullOrEmpty()) {
emit(null)
} else {
emit(description.parseAsHtml().filterSpans().sanitize())
emit(description.parseAsHtml(imageGetter = imageGetter).filterSpans())
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), null)
@Deprecated("")
val description = details
.map { it?.description }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
val onMangaRemoved = MutableEventFlow<Manga>()
val isScrobblingAvailable: Boolean
@@ -165,9 +147,7 @@ class DetailsViewModel @Inject constructor(
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val relatedManga: StateFlow<List<MangaItemModel>> = doubleManga.map {
it?.remote
}.distinctUntilChangedBy { it?.id }
val relatedManga: StateFlow<List<MangaItemModel>> = manga
.mapLatest {
if (it != null && settings.isRelatedMangaEnabled) {
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
@@ -178,40 +158,32 @@ class DetailsViewModel @Inject constructor(
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
val branches: StateFlow<List<MangaBranch>> = combine(
doubleManga,
details,
selectedBranch,
) { m, b ->
val chapters = m?.chapters
if (chapters.isNullOrEmpty()) return@combine emptyList()
chapters.groupBy { x -> x.branch }
(m?.chapters ?: return@combine emptyList())
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
.sortedWith(BranchComparator())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val isChaptersEmpty: StateFlow<Boolean> = combine(
doubleManga,
isLoading,
) { manga, loading ->
manga?.any != null && manga.chapters.isNullOrEmpty() && !loading
val isChaptersEmpty: StateFlow<Boolean> = details.map {
it != null && it.isLoaded && it.allChapters.isEmpty()
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
val chapters = combine(
combine(
doubleManga,
details,
history,
selectedBranch,
newChaptersCount,
bookmarks,
networkState,
) { manga, history, branch, news, bookmarks, isOnline ->
mapChapters(
manga?.remote?.takeIf { isOnline },
manga?.local,
) { manga, history, branch, news, bookmarks ->
manga?.mapChapters(
history,
news,
branch,
bookmarks,
)
).orEmpty()
},
isChaptersReversed,
chaptersQuery,
@@ -234,6 +206,13 @@ class DetailsViewModel @Inject constructor(
onShowTip.call(Unit)
}
}
launchJob(Dispatchers.Default) {
val manga = details.firstOrNull { !it?.chapters.isNullOrEmpty() } ?: return@launchJob
val h = history.firstOrNull()
if (h != null) {
progressUpdateUseCase(manga.toManga())
}
}
}
fun reload() {
@@ -242,7 +221,7 @@ class DetailsViewModel @Inject constructor(
}
fun deleteLocal() {
val m = doubleManga.value?.local
val m = details.value?.local?.manga
if (m == null) {
onShowToast.call(R.string.file_not_found)
return
@@ -295,13 +274,13 @@ class DetailsViewModel @Inject constructor(
fun markChapterAsCurrent(chapterId: Long) {
launchJob(Dispatchers.Default) {
val manga = checkNotNull(doubleManga.value)
val chapters = checkNotNull(manga.filterChapters(selectedBranchValue).chapters)
val manga = checkNotNull(details.value)
val chapters = checkNotNull(manga.chapters[selectedBranchValue])
val chapterIndex = chapters.indexOfFirst { it.id == chapterId }
check(chapterIndex in chapters.indices) { "Chapter not found" }
val percent = chapterIndex / chapters.size.toFloat()
historyRepository.addOrUpdate(
manga = manga.requireAny(),
manga = manga.toManga(),
chapterId = chapterId,
page = 0,
scroll = 0,
@@ -313,7 +292,7 @@ class DetailsViewModel @Inject constructor(
fun download(chaptersIds: Set<Long>?) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(
doubleManga.requireValue().requireAny(),
details.requireValue().toManga(),
chaptersIds,
)
onDownloadStarted.call(Unit)
@@ -333,14 +312,14 @@ class DetailsViewModel @Inject constructor(
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
doubleMangaLoadUseCase.invoke(intent)
detailsLoadUseCase.invoke(intent)
.onFirst {
val manga = it.requireAny()
val manga = it.toManga()
// find default branch
val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist)
}.collect {
doubleManga.value = it
details.value = it
}
}
@@ -356,21 +335,12 @@ class DetailsViewModel @Inject constructor(
private suspend fun onDownloadComplete(downloadedManga: LocalManga?) {
downloadedManga ?: return
launchJob {
doubleManga.update {
details.update {
interactor.updateLocal(it, downloadedManga)
}
}
}
private fun Spanned.filterSpans(): CharSequence {
val spannable = SpannableString.valueOf(this)
val spans = spannable.getSpans<ForegroundColorSpan>()
for (span in spans) {
spannable.removeSpan(span)
}
return spannable.trim()
}
private fun getScrobbler(index: Int): Scrobbler? {
val info = scrobblingInfo.value.getOrNull(index)
val scrobbler = if (info != null) {

View File

@@ -6,6 +6,7 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.textAndVisible
@@ -47,7 +48,6 @@ fun chapterListItemAD(
}
binding.imageViewBookmarked.isVisible = item.isBookmarked
binding.imageViewDownloaded.isVisible = item.isDownloaded
// binding.imageViewNew.isVisible = item.isNew
binding.textViewTitle.drawableStart = if (item.isNew) {
ContextCompat.getDrawable(context, R.drawable.ic_new)
} else {

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -92,19 +93,25 @@ class MangaSourcesRepository @Inject constructor(
}
}
fun observeNewSources(): Flow<Set<MangaSource>> = combine(
dao.observeAll(),
observeIsNsfwDisabled(),
) { entities, skipNsfw ->
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
result.remove(MangaSource(e.source))
fun observeNewSources(): Flow<Set<MangaSource>> = observeIsNewSourcesEnabled().flatMapLatest {
if (it) {
combine(
dao.observeAll(),
observeIsNsfwDisabled(),
) { entities, skipNsfw ->
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
result.remove(MangaSource(e.source))
}
if (skipNsfw) {
result.removeAll { x -> x.isNsfw() }
}
result
}.distinctUntilChanged()
} else {
flowOf(emptySet())
}
if (skipNsfw) {
result.removeAll { x -> x.isNsfw() }
}
result
}.distinctUntilChanged()
}
suspend fun assimilateNewSources(): Set<MangaSource> {
val new = getNewSources()
@@ -156,4 +163,8 @@ class MangaSourcesRepository @Inject constructor(
private fun observeIsNsfwDisabled() = settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) {
isNsfwContentDisabled
}
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
isNewSourcesTipEnabled
}
}

View File

@@ -1,17 +1,16 @@
package org.koitharu.kotatsu.favourites.data
import org.koitharu.kotatsu.core.db.entity.SortOrder
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
import java.util.Date
fun FavouriteCategoryEntity.toFavouriteCategory(id: Long = categoryId.toLong()) = FavouriteCategory(
id = id,
title = title,
sortKey = sortKey,
order = SortOrder(order, SortOrder.NEWEST),
order = ListSortOrder(order, ListSortOrder.NEWEST),
createdAt = Date(createdAt),
isTrackingEnabled = track,
isVisibleInLibrary = isVisibleInLibrary,

View File

@@ -1,13 +1,19 @@
package org.koitharu.kotatsu.favourites.data
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
@Dao
abstract class FavouritesDao {
@@ -22,7 +28,7 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
fun observeAll(order: SortOrder): Flow<List<FavouriteManga>> {
fun observeAll(order: ListSortOrder): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
@@ -47,7 +53,7 @@ abstract class FavouritesDao {
)
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
fun observeAll(categoryId: Long, order: SortOrder): Flow<List<FavouriteManga>> {
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
@@ -72,13 +78,14 @@ abstract class FavouritesDao {
)
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
suspend fun findCovers(categoryId: Long, order: SortOrder): List<Cover> {
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT m.cover_url AS url, m.source AS source FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
"SELECT manga.cover_url AS url, manga.source AS source FROM favourites " +
"LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE favourites.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
arrayOf<Any>(categoryId),
)
return findCoversImpl(query)
@@ -157,13 +164,12 @@ abstract class FavouritesDao {
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
private fun getOrderBy(sortOrder: SortOrder) = when (sortOrder) {
SortOrder.RATING -> "rating DESC"
SortOrder.NEWEST,
SortOrder.UPDATED,
-> "created_at DESC"
SortOrder.ALPHABETICAL -> "title ASC"
private fun getOrderBy(sortOrder: ListSortOrder) = when (sortOrder) {
ListSortOrder.RATING -> "manga.rating DESC"
ListSortOrder.NEWEST -> "favourites.created_at DESC"
ListSortOrder.ALPHABETIC -> "manga.title ASC"
ListSortOrder.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id) DESC"
ListSortOrder.PROGRESS -> "(SELECT percent FROM history WHERE history.manga_id = manga.manga_id) DESC"
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
}
}

View File

@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.SortOrder
import org.koitharu.kotatsu.core.db.entity.toEntities
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -20,8 +19,8 @@ import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.favourites.data.toManga
import org.koitharu.kotatsu.favourites.data.toMangaList
import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
import javax.inject.Inject
@@ -41,7 +40,7 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList()
}
fun observeAll(order: SortOrder): Flow<List<Manga>> {
fun observeAll(order: ListSortOrder): Flow<List<Manga>> {
return db.favouritesDao.observeAll(order)
.mapItems { it.toManga() }
}
@@ -51,7 +50,7 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList()
}
fun observeAll(categoryId: Long, order: SortOrder): Flow<List<Manga>> {
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<Manga>> {
return db.favouritesDao.observeAll(categoryId, order)
.mapItems { it.toManga() }
}
@@ -105,7 +104,7 @@ class FavouritesRepository @Inject constructor(
suspend fun createCategory(
title: String,
sortOrder: SortOrder,
sortOrder: ListSortOrder,
isTrackerEnabled: Boolean,
isVisibleOnShelf: Boolean,
): FavouriteCategory {
@@ -128,7 +127,7 @@ class FavouritesRepository @Inject constructor(
suspend fun updateCategory(
id: Long,
title: String,
sortOrder: SortOrder,
sortOrder: ListSortOrder,
isTrackerEnabled: Boolean,
isVisibleOnShelf: Boolean,
) {
@@ -156,7 +155,7 @@ class FavouritesRepository @Inject constructor(
}
}
suspend fun setCategoryOrder(id: Long, order: SortOrder) {
suspend fun setCategoryOrder(id: Long, order: ListSortOrder) {
db.favouriteCategoriesDao.updateOrder(id, order.name)
}
@@ -205,10 +204,10 @@ class FavouritesRepository @Inject constructor(
return ReversibleHandle { recoverToCategory(categoryId, ids) }
}
private fun observeOrder(categoryId: Long): Flow<SortOrder> {
private fun observeOrder(categoryId: Long): Flow<ListSortOrder> {
return db.favouriteCategoriesDao.observe(categoryId)
.filterNotNull()
.map { x -> SortOrder(x.order, SortOrder.NEWEST) }
.map { x -> ListSortOrder(x.order, ListSortOrder.NEWEST) }
.distinctUntilChanged()
}

View File

@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEdit
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.SortOrder
import javax.inject.Inject
@AndroidEntryPoint
@@ -176,12 +175,6 @@ class FavouriteCategoriesActivity :
companion object {
val SORT_ORDERS = arrayOf(
SortOrder.ALPHABETICAL,
SortOrder.NEWEST,
SortOrder.RATING,
)
fun newIntent(context: Context) = Intent(context, FavouriteCategoriesActivity::class.java)
}
}

View File

@@ -18,16 +18,15 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.ui.util.DefaultTextWatcher
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getSerializableCompat
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.databinding.ActivityCategoryEditBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
import com.google.android.material.R as materialR
@AndroidEntryPoint
@@ -38,7 +37,8 @@ class FavouritesCategoryEditActivity :
DefaultTextWatcher {
private val viewModel by viewModels<FavouritesCategoryEditViewModel>()
private var selectedSortOrder: SortOrder? = null
private var selectedSortOrder: ListSortOrder? = null
private val sortOrders = ListSortOrder.FAVORITES.sortedByOrdinal()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -68,7 +68,7 @@ class FavouritesCategoryEditActivity :
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val order = savedInstanceState.getSerializableCompat<SortOrder>(KEY_SORT_ORDER)
val order = savedInstanceState.getSerializableCompat<ListSortOrder>(KEY_SORT_ORDER)
if (order != null) {
selectedSortOrder = order
}
@@ -103,7 +103,7 @@ class FavouritesCategoryEditActivity :
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedSortOrder = FavouriteCategoriesActivity.SORT_ORDERS.getOrNull(position)
selectedSortOrder = sortOrders.getOrNull(position)
}
private fun onCategoryChanged(category: FavouriteCategory?) {
@@ -113,7 +113,7 @@ class FavouritesCategoryEditActivity :
}
viewBinding.editName.setText(category?.title)
selectedSortOrder = category?.order
val sortText = getString((category?.order ?: SortOrder.NEWEST).titleRes)
val sortText = getString((category?.order ?: ListSortOrder.NEWEST).titleResId)
viewBinding.editSort.setText(sortText, false)
viewBinding.switchTracker.setChecked(category?.isTrackingEnabled ?: true, false)
viewBinding.switchShelf.setChecked(category?.isVisibleInLibrary ?: true, false)
@@ -135,17 +135,17 @@ class FavouritesCategoryEditActivity :
}
private fun initSortSpinner() {
val entries = FavouriteCategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) }
val entries = sortOrders.map { getString(it.titleResId) }
val adapter = SortAdapter(this, entries)
viewBinding.editSort.setAdapter(adapter)
viewBinding.editSort.onItemClickListener = this
}
private fun getSelectedSortOrder(): SortOrder {
private fun getSelectedSortOrder(): ListSortOrder {
selectedSortOrder?.let { return it }
val entries = FavouriteCategoriesActivity.SORT_ORDERS.map { getString(it.titleRes) }
val entries = sortOrders.map { getString(it.titleResId) }
val index = entries.indexOf(viewBinding.editSort.text.toString())
return FavouriteCategoriesActivity.SORT_ORDERS.getOrNull(index) ?: SortOrder.NEWEST
return sortOrders.getOrNull(index) ?: ListSortOrder.NEWEST
}
private class SortAdapter(

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.EXTRA_ID
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity.Companion.NO_ID
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
import javax.inject.Inject
@HiltViewModel
@@ -48,7 +48,7 @@ class FavouritesCategoryEditViewModel @Inject constructor(
fun save(
title: String,
sortOrder: SortOrder,
sortOrder: ListSortOrder,
isTrackerEnabled: Boolean,
isVisibleOnShelf: Boolean,
) {

View File

@@ -10,13 +10,11 @@ import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -27,12 +25,14 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
override val isSwipeRefreshEnabled = false
val categoryId
get() = viewModel.categoryId
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
if (viewModel.categoryId != NO_ID) {
addMenuProvider(FavouritesListMenuProvider(binding.root.context, viewModel))
}
viewModel.sortOrder.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
}
override fun onScrolledToEnd() = Unit
@@ -40,14 +40,15 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
override fun onFilterClick(view: View?) {
val menu = PopupMenu(view?.context ?: return, view)
menu.setOnMenuItemClickListener(this)
for ((i, item) in FavouriteCategoriesActivity.SORT_ORDERS.withIndex()) {
menu.menu.add(Menu.NONE, Menu.NONE, i, item.titleRes)
val orders = ListSortOrder.FAVORITES.sortedByOrdinal()
for ((i, item) in orders.withIndex()) {
menu.menu.add(Menu.NONE, Menu.NONE, i, item.titleResId)
}
menu.show()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
val order = FavouriteCategoriesActivity.SORT_ORDERS.getOrNull(item.order) ?: return false
val order = ListSortOrder.FAVORITES.sortedByOrdinal().getOrNull(item.order) ?: return false
viewModel.setSortOrder(order)
return true
}

View File

@@ -5,12 +5,8 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.core.view.forEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.model.titleRes
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.parsers.model.SortOrder
class FavouritesListMenuProvider(
private val context: Context,
@@ -19,34 +15,12 @@ class FavouritesListMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_favourites, menu)
val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return
for (order in FavouriteCategoriesActivity.SORT_ORDERS) {
subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleRes)
}
subMenu.setGroupCheckable(R.id.group_order, true, true)
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
val order = viewModel.sortOrder.value ?: return
menu.findItem(R.id.action_order)?.subMenu?.forEach { item ->
if (item.order == order.ordinal) {
item.isChecked = true
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.groupId == R.id.group_order) {
val order = SortOrder.entries[menuItem.order]
viewModel.setSortOrder(order)
return true
}
return when (menuItem.itemId) {
R.id.action_edit -> {
context.startActivity(
FavouritesCategoryEditActivity.newIntent(context, viewModel.categoryId),
)
context.startActivity(FavouritesCategoryEditActivity.newIntent(context, viewModel.categoryId))
true
}

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
@@ -21,12 +22,12 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState
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.parsers.model.SortOrder
import javax.inject.Inject
@HiltViewModel
@@ -40,7 +41,10 @@ class FavouritesListViewModel @Inject constructor(
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
val sortOrder: StateFlow<SortOrder?> = if (categoryId == NO_ID) {
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_FAVORITES) { favoritesListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.favoritesListMode)
val sortOrder: StateFlow<ListSortOrder?> = if (categoryId == NO_ID) {
MutableStateFlow(null)
} else {
repository.observeCategory(categoryId)
@@ -50,7 +54,7 @@ class FavouritesListViewModel @Inject constructor(
override val content = combine(
if (categoryId == NO_ID) {
repository.observeAll(SortOrder.NEWEST)
repository.observeAll(ListSortOrder.NEWEST)
} else {
repository.observeAll(categoryId)
},
@@ -94,7 +98,7 @@ class FavouritesListViewModel @Inject constructor(
}
}
fun setSortOrder(order: SortOrder) {
fun setSortOrder(order: ListSortOrder) {
if (categoryId == NO_ID) {
return
}

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterItem
import org.koitharu.kotatsu.filter.ui.model.FilterState
@@ -207,7 +208,7 @@ class FilterCoordinator @Inject constructor(
state: FilterState,
query: String,
): List<ListModel> {
val sortOrders = repository.sortOrders.sortedBy { it.ordinal }
val sortOrders = repository.sortOrders.sortedByOrdinal()
val tags = mergeTags(state.tags, allTags.tags).toList()
val list = ArrayList<ListModel>(tags.size + sortOrders.size + 3)
if (query.isEmpty()) {

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import org.koitharu.kotatsu.list.domain.ListSortOrder
@Dao
abstract class HistoryDao {
@@ -33,12 +33,14 @@ abstract class HistoryDao {
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
fun observeAll(order: HistoryOrder): Flow<List<HistoryWithManga>> {
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
val orderBy = when (order) {
HistoryOrder.UPDATED -> "history.updated_at DESC"
HistoryOrder.CREATED -> "history.created_at DESC"
HistoryOrder.PROGRESS -> "history.percent DESC"
HistoryOrder.ALPHABETIC -> "manga.title"
ListSortOrder.UPDATED -> "history.updated_at DESC"
ListSortOrder.NEWEST -> "history.created_at DESC"
ListSortOrder.PROGRESS -> "history.percent DESC"
ListSortOrder.ALPHABETIC -> "manga.title"
ListSortOrder.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id) DESC"
else -> throw IllegalArgumentException("Sort order $order is not supported")
}
@Language("RoomSql")

View File

@@ -18,8 +18,8 @@ import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
@@ -66,7 +66,7 @@ class HistoryRepository @Inject constructor(
}
}
fun observeAllWithHistory(order: HistoryOrder): Flow<List<MangaWithHistory>> {
fun observeAllWithHistory(order: ListSortOrder): Flow<List<MangaWithHistory>> {
return db.historyDao.observeAll(order).mapItems {
MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()),
@@ -93,8 +93,11 @@ class HistoryRepository @Inject constructor(
}
val tags = manga.tags.toEntities()
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags)
val existing = db.mangaDao.find(manga.id)?.manga
if (existing == null || existing.source == manga.source.name) {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags)
}
db.historyDao.upsert(
HistoryEntity(
mangaId = manga.id,

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.history.domain.model
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
enum class HistoryOrder(
@StringRes val titleResId: Int,
) {
UPDATED(R.string.updated),
CREATED(R.string.order_added),
PROGRESS(R.string.progress),
ALPHABETIC(R.string.by_name);
fun isGroupingSupported() = this == UPDATED || this == CREATED || this == PROGRESS
}

View File

@@ -9,9 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
@@ -26,9 +24,6 @@ class HistoryListFragment : MangaListFragment() {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
val menuInvalidator = MenuInvalidator(requireActivity())
viewModel.isGroupingEnabled.observe(viewLifecycleOwner, menuInvalidator)
viewModel.sortOrder.observe(viewLifecycleOwner, menuInvalidator)
}
override fun onScrolledToEnd() = Unit

View File

@@ -5,12 +5,10 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.core.view.forEach
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.startOfDay
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import java.util.Date
import java.util.concurrent.TimeUnit
import com.google.android.material.R as materialR
@@ -22,47 +20,19 @@ class HistoryListMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_history, menu)
val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return
for (order in HistoryOrder.entries) {
subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleResId)
}
subMenu.setGroupCheckable(R.id.group_order, true, true)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
if (menuItem.groupId == R.id.group_order) {
val order = HistoryOrder.entries[menuItem.order]
viewModel.setSortOrder(order)
return true
}
return when (menuItem.itemId) {
R.id.action_clear_history -> {
showClearHistoryDialog()
true
}
R.id.action_history_grouping -> {
viewModel.setGrouping(!menuItem.isChecked)
true
}
else -> false
}
}
override fun onPrepareMenu(menu: Menu) {
val order = viewModel.sortOrder.value
menu.findItem(R.id.action_order)?.subMenu?.forEach { item ->
if (item.order == order.ordinal) {
item.isChecked = true
}
}
menu.findItem(R.id.action_history_grouping)?.run {
isChecked = viewModel.isGroupingEnabled.value == true
isEnabled = order.isGroupingSupported()
}
}
private fun showClearHistoryDialog() {
val selectionListener = RememberSelectionDialogListener(2)
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)

View File

@@ -26,9 +26,9 @@ import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.EmptyState
@@ -54,22 +54,21 @@ class HistoryListViewModel @Inject constructor(
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
val sortOrder: StateFlow<HistoryOrder> = settings.observeAsStateFlow(
private val sortOrder: StateFlow<ListSortOrder> = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.IO,
key = AppSettings.KEY_HISTORY_ORDER,
valueProducer = { historySortOrder },
)
val isGroupingEnabled = settings.observeAsFlow(
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_HISTORY) { historyListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.historyListMode)
private val isGroupingEnabled = settings.observeAsFlow(
key = AppSettings.KEY_HISTORY_GROUPING,
valueProducer = { isHistoryGroupingEnabled },
).combine(sortOrder) { g, s ->
g && s.isGroupingSupported()
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.Eagerly,
initialValue = settings.isHistoryGroupingEnabled && sortOrder.value.isGroupingSupported(),
)
}
override val content = combine(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
@@ -101,10 +100,6 @@ class HistoryListViewModel @Inject constructor(
override fun onRetry() = Unit
fun setSortOrder(order: HistoryOrder) {
settings.historySortOrder = order
}
fun clearHistory(minDate: Long) {
launchJob(Dispatchers.Default) {
val stringRes = if (minDate <= 0) {
@@ -128,10 +123,6 @@ class HistoryListViewModel @Inject constructor(
}
}
fun setGrouping(isGroupingEnabled: Boolean) {
settings.isHistoryGroupingEnabled = isGroupingEnabled
}
private suspend fun mapList(
list: List<MangaWithHistory>,
grouped: Boolean,
@@ -173,10 +164,10 @@ class HistoryListViewModel @Inject constructor(
return result
}
private fun MangaHistory.header(order: HistoryOrder): ListHeader? = when (order) {
HistoryOrder.UPDATED -> ListHeader(timeAgo(updatedAt))
HistoryOrder.CREATED -> ListHeader(timeAgo(createdAt))
HistoryOrder.PROGRESS -> ListHeader(
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
ListSortOrder.UPDATED -> ListHeader(timeAgo(updatedAt))
ListSortOrder.NEWEST -> ListHeader(timeAgo(createdAt))
ListSortOrder.PROGRESS -> ListHeader(
when (percent) {
1f -> R.string.status_completed
in 0f..0.01f -> R.string.status_planned
@@ -185,7 +176,10 @@ class HistoryListViewModel @Inject constructor(
},
)
HistoryOrder.ALPHABETIC -> null
ListSortOrder.ALPHABETIC,
ListSortOrder.RELEVANCE,
ListSortOrder.NEW_CHAPTERS,
ListSortOrder.RATING -> null
}
private fun timeAgo(date: Date): DateTimeAgo {

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.list.domain
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.find
import java.util.EnumSet
enum class ListSortOrder(
@StringRes val titleResId: Int,
) {
UPDATED(R.string.updated),
NEWEST(R.string.order_added),
PROGRESS(R.string.progress),
ALPHABETIC(R.string.by_name),
RATING(R.string.by_rating),
RELEVANCE(R.string.by_relevance),
NEW_CHAPTERS(R.string.new_chapters),
;
fun isGroupingSupported() = this == UPDATED || this == NEWEST || this == PROGRESS
companion object {
val HISTORY: Set<ListSortOrder> = EnumSet.of(UPDATED, NEWEST, PROGRESS, ALPHABETIC, NEW_CHAPTERS)
val FAVORITES: Set<ListSortOrder> = EnumSet.of(ALPHABETIC, NEWEST, RATING, NEW_CHAPTERS, PROGRESS)
val SUGGESTIONS: Set<ListSortOrder> = EnumSet.of(RELEVANCE)
operator fun invoke(value: String, fallback: ListSortOrder) = entries.find(value) ?: fallback
}
}

View File

@@ -1,78 +0,0 @@
package org.koitharu.kotatsu.list.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
import org.koitharu.kotatsu.databinding.DialogListModeBinding
import javax.inject.Inject
@AndroidEntryPoint
class ListModeBottomSheet :
BaseAdaptiveSheet<DialogListModeBinding>(),
Slider.OnChangeListener,
MaterialButtonToggleGroup.OnButtonCheckedListener {
@Inject
lateinit var settings: AppSettings
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = DialogListModeBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: DialogListModeBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val mode = settings.listMode
binding.buttonList.isChecked = mode == ListMode.LIST
binding.buttonListDetailed.isChecked = mode == ListMode.DETAILED_LIST
binding.buttonGrid.isChecked = mode == ListMode.GRID
binding.textViewGridTitle.isVisible = mode == ListMode.GRID
binding.sliderGrid.isVisible = mode == ListMode.GRID
binding.sliderGrid.setLabelFormatter(IntPercentLabelFormatter(binding.root.context))
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
binding.sliderGrid.addOnChangeListener(this)
binding.checkableGroup.addOnButtonCheckedListener(this)
}
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
if (!isChecked) {
return
}
val mode = when (checkedId) {
R.id.button_list -> ListMode.LIST
R.id.button_list_detailed -> ListMode.DETAILED_LIST
R.id.button_grid -> ListMode.GRID
else -> return
}
requireViewBinding().textViewGridTitle.isVisible = mode == ListMode.GRID
requireViewBinding().sliderGrid.isVisible = mode == ListMode.GRID
settings.listMode = mode
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
settings.gridSize = value.toInt()
}
}
companion object {
private const val TAG = "ListModeSelectDialog"
fun show(fm: FragmentManager) = ListModeBottomSheet().showDistinct(fm, TAG)
}
}

View File

@@ -6,6 +6,11 @@ import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet
import org.koitharu.kotatsu.list.ui.config.ListConfigSection
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
class MangaListMenuProvider(
private val fragment: Fragment,
@@ -17,7 +22,13 @@ class MangaListMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_list_mode -> {
ListModeBottomSheet.show(fragment.childFragmentManager)
val section: ListConfigSection = when (fragment) {
is HistoryListFragment -> ListConfigSection.History
is SuggestionsFragment -> ListConfigSection.Suggestions
is FavouritesListFragment -> ListConfigSection.Favorites(fragment.categoryId)
else -> ListConfigSection.General
}
ListConfigBottomSheet.show(fragment.childFragmentManager, section)
true
}

View File

@@ -24,7 +24,7 @@ abstract class MangaListViewModel(
) : BaseViewModel() {
abstract val content: StateFlow<List<ListModel>>
val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
open val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.listMode)
val onActionDone = MutableEventFlow<ReversibleAction>()
val gridScale = settings.observeAsStateFlow(

View File

@@ -0,0 +1,132 @@
package org.koitharu.kotatsu.list.ui.config
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
import org.koitharu.kotatsu.databinding.SheetListModeBinding
import javax.inject.Inject
@AndroidEntryPoint
class ListConfigBottomSheet :
BaseAdaptiveSheet<SheetListModeBinding>(),
Slider.OnChangeListener,
MaterialButtonToggleGroup.OnButtonCheckedListener, CompoundButton.OnCheckedChangeListener,
AdapterView.OnItemSelectedListener {
@Inject
@Deprecated("")
lateinit var settings: AppSettings
private val viewModel by viewModels<ListConfigViewModel>()
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = SheetListModeBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: SheetListModeBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val mode = viewModel.listMode
binding.buttonList.isChecked = mode == ListMode.LIST
binding.buttonListDetailed.isChecked = mode == ListMode.DETAILED_LIST
binding.buttonGrid.isChecked = mode == ListMode.GRID
binding.textViewGridTitle.isVisible = mode == ListMode.GRID
binding.sliderGrid.isVisible = mode == ListMode.GRID
binding.sliderGrid.setLabelFormatter(IntPercentLabelFormatter(binding.root.context))
binding.sliderGrid.setValueRounded(viewModel.gridSize.toFloat())
binding.sliderGrid.addOnChangeListener(this)
binding.checkableGroup.addOnButtonCheckedListener(this)
binding.switchGrouping.isVisible = viewModel.isGroupingAvailable
if (viewModel.isGroupingAvailable) {
binding.switchGrouping.isEnabled = settings.historySortOrder.isGroupingSupported()
}
binding.switchGrouping.isChecked = settings.isHistoryGroupingEnabled
binding.switchGrouping.setOnCheckedChangeListener(this)
val sortOrders = viewModel.getSortOrders()
if (sortOrders != null) {
binding.textViewOrderTitle.isVisible = true
binding.spinnerOrder.adapter = ArrayAdapter(
binding.spinnerOrder.context,
android.R.layout.simple_spinner_dropdown_item,
android.R.id.text1,
sortOrders.map { binding.spinnerOrder.context.getString(it.titleResId) },
)
val selected = sortOrders.indexOf(viewModel.getSelectedSortOrder())
if (selected >= 0) {
binding.spinnerOrder.setSelection(selected, false)
}
binding.spinnerOrder.onItemSelectedListener = this
binding.cardOrder.isVisible = true
}
}
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
if (!isChecked) {
return
}
val mode = when (checkedId) {
R.id.button_list -> ListMode.LIST
R.id.button_list_detailed -> ListMode.DETAILED_LIST
R.id.button_grid -> ListMode.GRID
else -> return
}
requireViewBinding().textViewGridTitle.isVisible = mode == ListMode.GRID
requireViewBinding().sliderGrid.isVisible = mode == ListMode.GRID
viewModel.listMode = mode
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
when (buttonView.id) {
R.id.switch_grouping -> settings.isHistoryGroupingEnabled = isChecked
}
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
viewModel.gridSize = value.toInt()
}
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
when (parent.id) {
R.id.spinner_order -> {
viewModel.setSortOrder(position)
viewBinding?.switchGrouping?.isEnabled = settings.historySortOrder.isGroupingSupported()
}
}
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
companion object {
private const val TAG = "ListModeSelectDialog"
const val ARG_SECTION = "section"
fun show(fm: FragmentManager, section: ListConfigSection) = ListConfigBottomSheet().withArgs(1) {
putParcelable(ARG_SECTION, section)
}.showDistinct(fm, TAG)
}
}

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.list.ui.config
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed interface ListConfigSection : Parcelable {
@Parcelize
data object History : ListConfigSection
@Parcelize
data object General : ListConfigSection
@Parcelize
data class Favorites(
val categoryId: Long,
) : ListConfigSection
@Parcelize
data object Suggestions : ListConfigSection
}

View File

@@ -0,0 +1,76 @@
package org.koitharu.kotatsu.list.ui.config
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.list.domain.ListSortOrder
import javax.inject.Inject
@HiltViewModel
class ListConfigViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val settings: AppSettings,
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
val section = savedStateHandle.require<ListConfigSection>(ListConfigBottomSheet.ARG_SECTION)
var listMode: ListMode
get() = when (section) {
is ListConfigSection.Favorites -> settings.favoritesListMode
ListConfigSection.General -> settings.listMode
ListConfigSection.History -> settings.historyListMode
ListConfigSection.Suggestions -> settings.suggestionsListMode
}
set(value) {
when (section) {
is ListConfigSection.Favorites -> settings.favoritesListMode = value
ListConfigSection.General -> settings.listMode = value
ListConfigSection.History -> settings.historyListMode = value
ListConfigSection.Suggestions -> settings.suggestionsListMode = value
}
}
var gridSize: Int
get() = settings.gridSize
set(value) {
settings.gridSize = value
}
val isGroupingAvailable: Boolean
get() = section == ListConfigSection.History
fun getSortOrders(): List<ListSortOrder>? = when (section) {
is ListConfigSection.Favorites -> ListSortOrder.FAVORITES
ListConfigSection.General -> null
ListConfigSection.History -> ListSortOrder.HISTORY
ListConfigSection.Suggestions -> ListSortOrder.SUGGESTIONS
}?.sortedByOrdinal()
fun getSelectedSortOrder(): ListSortOrder? = when (section) {
is ListConfigSection.Favorites -> runBlocking { favouritesRepository.getCategory(section.categoryId).order }
ListConfigSection.General -> null
ListConfigSection.History -> settings.historySortOrder
ListConfigSection.Suggestions -> ListSortOrder.RELEVANCE // TODO
}
fun setSortOrder(position: Int) {
val value = getSortOrders()?.getOrNull(position) ?: return
when (section) {
is ListConfigSection.Favorites -> launchJob {
favouritesRepository.setCategoryOrder(section.categoryId, value)
}
ListConfigSection.General -> Unit
ListConfigSection.History -> settings.historySortOrder = value
ListConfigSection.Suggestions -> Unit
}
}
}

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeMutex
import org.koitharu.kotatsu.core.util.CompositeMutex2
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.filterWith
@@ -45,7 +45,7 @@ class LocalMangaRepository @Inject constructor(
) : MangaRepository {
override val source = MangaSource.LOCAL
private val locks = CompositeMutex<Long>()
private val locks = CompositeMutex2<Long>()
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
@@ -122,14 +122,18 @@ class LocalMangaRepository @Inject constructor(
suspend fun getRemoteManga(localManga: Manga): Manga? {
return runCatchingCancellable {
LocalMangaInput.of(localManga).getMangaInfo()
LocalMangaInput.of(localManga).getMangaInfo()?.takeUnless { it.isLocal }
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
}
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
// TODO fast path by name
// fast path
LocalMangaInput.find(storageManager.getReadableDirs(), remoteManga)?.let {
return it.getManga()
}
// slow path
val files = getAllFiles()
return channelFlow {
for (file in files) {

View File

@@ -4,6 +4,7 @@ import androidx.annotation.WorkerThread
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -21,7 +22,8 @@ class MangaIndex(source: String?) {
private val json: JSONObject = source?.let(::JSONObject) ?: JSONObject()
fun setMangaInfo(manga: Manga, append: Boolean) {
fun setMangaInfo(manga: Manga) {
require(!manga.isLocal) { "Local manga information cannot be stored" }
json.put("id", manga.id)
json.put("title", manga.title)
json.put("title_alt", manga.altTitle)
@@ -46,7 +48,7 @@ class MangaIndex(source: String?) {
}
},
)
if (!append || !json.has("chapters")) {
if (!json.has("chapters")) {
json.put("chapters", JSONObject())
}
json.put("app_id", BuildConfig.APPLICATION_ID)

View File

@@ -2,12 +2,18 @@ package org.koitharu.kotatsu.local.data.input
import android.net.Uri
import androidx.core.net.toFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File
sealed class LocalMangaInput(
@@ -37,6 +43,24 @@ sealed class LocalMangaInput(
else -> null
}
suspend fun find(roots: Iterable<File>, manga: Manga): LocalMangaInput? = channelFlow {
val fileName = manga.title.toFileNameSafe()
for (root in roots) {
launch {
val dir = File(root, fileName)
val zip = File(root, "$fileName.cbz")
val input = when {
dir.isDirectory -> LocalMangaDirInput(dir)
zip.isFile -> LocalMangaZipInput(zip)
else -> null
}
if (input?.getMangaInfo()?.id == manga.id) {
send(input)
}
}
}
}.flowOn(Dispatchers.Default).firstOrNull()
@JvmStatic
protected fun zipUri(file: File, entryName: String): String =
Uri.fromParts("cbz", file.path, entryName).toString()

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.data.output
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.zip.ZipOutput
@@ -21,7 +22,9 @@ class LocalMangaDirOutput(
private val index = MangaIndex(File(rootFile, ENTRY_NAME_INDEX).takeIfReadable()?.readText())
init {
index.setMangaInfo(manga, append = true)
if (!manga.isLocal) {
index.setMangaInfo(manga)
}
}
override suspend fun mergeWithExisting() = Unit

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.data.output
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.readText
import org.koitharu.kotatsu.core.zip.ZipOutput
@@ -21,7 +22,9 @@ class LocalMangaZipOutput(
private val index = MangaIndex(null)
init {
index.setMangaInfo(manga, false)
if (!manga.isLocal) {
index.setMangaInfo(manga)
}
}
override suspend fun mergeWithExisting() {

View File

@@ -47,7 +47,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
startForeground()
val mangaWithChapters = localMangaRepository.getDetails(manga)
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
localStorageChanges.emit(LocalManga(manga))
localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga)))
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
}

View File

@@ -24,7 +24,7 @@ class LocalListMenuProvider(
true
}
R.id.action_settings -> {
R.id.action_directories -> {
context.startActivity(MangaDirectoriesActivity.newIntent(context))
true
}

View File

@@ -2,16 +2,10 @@ package org.koitharu.kotatsu.reader.domain
import android.util.LongSparseArray
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import javax.inject.Inject
@@ -23,32 +17,24 @@ class ChaptersLoader @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
private val chapters = MutableStateFlow(LongSparseArray<MangaChapter>(0))
private val chapters = LongSparseArray<MangaChapter>()
private val chapterPages = ChapterPages()
private val mutex = Mutex()
val size: Int // TODO flow
get() = chapters.value.size()
val size: Int
get() = chapters.size()
fun init(scope: CoroutineScope, manga: Flow<DoubleManga>) = scope.launch {
manga.collect {
val ch = it.chapters.orEmpty()
val longSparseArray = LongSparseArray<MangaChapter>(ch.size)
ch.forEach { x -> longSparseArray.put(x.id, x) }
mutex.withLock {
chapters.value = longSparseArray
}
suspend fun init(manga: MangaDetails) = mutex.withLock {
chapters.clear()
manga.allChapters.forEach {
chapters.put(it.id, it)
}
}
suspend fun loadPrevNextChapter(manga: DoubleManga, currentId: Long, isNext: Boolean) {
val chapters = manga.chapters ?: return
suspend fun loadPrevNextChapter(manga: MangaDetails, currentId: Long, isNext: Boolean) {
val chapters = manga.allChapters
val predicate: (MangaChapter) -> Boolean = { it.id == currentId }
val index = if (isNext) {
chapters.indexOfFirst(predicate)
} else {
chapters.indexOfLast(predicate)
}
val index = if (isNext) chapters.indexOfFirst(predicate) else chapters.indexOfLast(predicate)
if (index == -1) return
val newChapter = chapters.getOrNull(if (isNext) index + 1 else index - 1) ?: return
val newPages = loadChapter(newChapter.id)
@@ -79,11 +65,7 @@ class ChaptersLoader @Inject constructor(
}
}
fun peekChapter(chapterId: Long): MangaChapter? = chapters.value[chapterId]
suspend fun awaitChapter(chapterId: Long): MangaChapter? = chapters.mapNotNull { x ->
x[chapterId]
}.firstOrNull()
fun peekChapter(chapterId: Long): MangaChapter? = chapters[chapterId]
fun getPages(chapterId: Long): List<ReaderPage> {
return chapterPages.subList(chapterId)
@@ -100,7 +82,7 @@ class ChaptersLoader @Inject constructor(
fun snapshot() = chapterPages.toList()
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
val chapter = checkNotNull(awaitChapter(chapterId)) { "Requested chapter not found" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
val repo = mangaRepositoryFactory.create(chapter.source)
return repo.getPages(chapter).mapIndexed { index, page ->
ReaderPage(page, index, chapterId)

View File

@@ -33,13 +33,11 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
): SheetChaptersBinding {
return SheetChaptersBinding.inflate(inflater, container, false)
}
) = SheetChaptersBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: SheetChaptersBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val chapters = viewModel.manga?.chapters
val chapters = viewModel.manga?.allChapters
if (chapters.isNullOrEmpty()) {
dismissAllowingStateLoss()
return
@@ -61,7 +59,7 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(),
val offset =
(resources.getDimensionPixelSize(R.dimen.chapter_list_item_height) * 0.6).roundToInt()
adapter.setItems(
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset)
items, RecyclerViewScrollCallback(binding.recyclerView, targetPosition, offset),
)
} else {
adapter.items = items

View File

@@ -189,7 +189,7 @@ class ReaderActivity :
val state = viewModel.getCurrentState() ?: return false
PagesThumbnailsSheet.show(
supportFragmentManager,
viewModel.manga?.any ?: return false,
viewModel.manga?.toManga() ?: return false,
state.chapterId,
state.page,
)

View File

@@ -44,12 +44,11 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
@@ -74,7 +73,7 @@ class ReaderViewModel @Inject constructor(
private val pageLoader: PageLoader,
private val chaptersLoader: ChaptersLoader,
private val appShortcutManager: AppShortcutManager,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
private val detailsLoadUseCase: DetailsLoadUseCase,
private val historyUpdateUseCase: HistoryUpdateUseCase,
private val detectReaderModeUseCase: DetectReaderModeUseCase,
) : BaseViewModel() {
@@ -88,9 +87,9 @@ class ReaderViewModel @Inject constructor(
private var bookmarkJob: Job? = null
private var stateChangeJob: Job? = null
private val currentState = MutableStateFlow<ReaderState?>(savedStateHandle[ReaderActivity.EXTRA_STATE])
private val mangaData = MutableStateFlow(intent.manga?.let { DoubleManga(it) })
private val mangaData = MutableStateFlow(intent.manga?.let { MangaDetails(it, null, null, false) })
private val mangaFlow: Flow<Manga?>
get() = mangaData.map { it?.any }
get() = mangaData.map { it?.toManga() }
val readerMode = MutableStateFlow<ReaderMode?>(null)
val onPageSaved = MutableEventFlow<Uri?>()
@@ -98,7 +97,7 @@ class ReaderViewModel @Inject constructor(
val uiState = MutableStateFlow<ReaderUiState?>(null)
val content = MutableStateFlow(ReaderContent(emptyList(), null))
val manga: DoubleManga?
val manga: MangaDetails?
get() = mangaData.value
val pageAnimation = settings.observeAsStateFlow(
@@ -148,7 +147,7 @@ class ReaderViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val isBookmarkAdded = currentState.flatMapLatest { state ->
val manga = mangaData.value?.any
val manga = mangaData.value?.toManga()
if (state == null || manga == null) {
flowOf(false)
} else {
@@ -178,7 +177,7 @@ class ReaderViewModel @Inject constructor(
fun switchMode(newMode: ReaderMode) {
launchJob {
val manga = checkNotNull(mangaData.value?.any)
val manga = checkNotNull(mangaData.value?.toManga())
dataRepository.saveReaderMode(
manga = manga,
mode = newMode,
@@ -199,7 +198,7 @@ class ReaderViewModel @Inject constructor(
}
val readerState = state ?: currentState.value ?: return
historyUpdateUseCase.invokeAsync(
manga = mangaData.value?.any ?: return,
manga = mangaData.value?.toManga() ?: return,
readerState = readerState,
percent = computePercent(readerState.chapterId, readerState.page),
)
@@ -295,7 +294,7 @@ class ReaderViewModel @Inject constructor(
val state = checkNotNull(currentState.value)
val page = checkNotNull(getCurrentPage()) { "Page not found" }
val bookmark = Bookmark(
manga = checkNotNull(mangaData.value?.any),
manga = mangaData.requireValue().toManga(),
pageId = page.id,
chapterId = state.chapterId,
page = state.page,
@@ -315,7 +314,7 @@ class ReaderViewModel @Inject constructor(
}
bookmarkJob = launchJob {
loadingJob?.join()
val manga = checkNotNull(mangaData.value?.any)
val manga = mangaData.requireValue().toManga()
val state = checkNotNull(getCurrentState())
bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
onShowToast.call(R.string.bookmark_removed)
@@ -324,25 +323,19 @@ class ReaderViewModel @Inject constructor(
private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default) {
var manga = DoubleManga(
dataRepository.resolveIntent(intent)
?: throw NotFoundException("Cannot find manga", ""),
)
mangaData.value = manga
val mangaFlow = doubleMangaLoadUseCase(intent)
manga = mangaFlow.first { x -> x.any != null }
chaptersLoader.init(viewModelScope, mangaFlow.withErrorHandling())
// determine mode
val singleManga = manga.requireAny()
val details = detailsLoadUseCase.invoke(intent).first { x -> x.isLoaded }
mangaData.value = details
chaptersLoader.init(details)
val manga = details.toManga()
// obtain state
if (currentState.value == null) {
currentState.value = historyRepository.getOne(singleManga)?.let {
currentState.value = historyRepository.getOne(manga)?.let {
ReaderState(it)
} ?: ReaderState(singleManga, preselectedBranch)
} ?: ReaderState(manga, preselectedBranch)
}
val mode = detectReaderModeUseCase.invoke(singleManga, currentState.value)
val branch = chaptersLoader.awaitChapter(currentState.value?.chapterId ?: 0L)?.branch
mangaData.value = manga.filterChapters(branch)
val mode = detectReaderModeUseCase.invoke(manga, currentState.value)
val branch = chaptersLoader.peekChapter(currentState.value?.chapterId ?: 0L)?.branch
mangaData.value = details.filterChapters(branch)
readerMode.value = mode
chaptersLoader.loadSingleChapter(requireNotNull(currentState.value).chapterId)
@@ -350,7 +343,7 @@ class ReaderViewModel @Inject constructor(
if (!isIncognito) {
currentState.value?.let {
val percent = computePercent(it.chapterId, it.page)
historyUpdateUseCase.invoke(singleManga, it, percent)
historyUpdateUseCase.invoke(manga, it, percent)
}
}
notifyStateChanged()
@@ -383,11 +376,11 @@ class ReaderViewModel @Inject constructor(
val state = getCurrentState()
val chapter = state?.chapterId?.let { chaptersLoader.peekChapter(it) }
val newState = ReaderUiState(
mangaName = manga?.any?.title,
mangaName = manga?.toManga()?.title,
branch = chapter?.branch,
chapterName = chapter?.name,
chapterNumber = chapter?.number ?: 0,
chaptersTotal = manga?.any?.getChapters(chapter?.branch)?.size ?: 0,
chaptersTotal = manga?.chapters?.get(chapter?.branch)?.size ?: 0,
totalPages = if (chapter != null) chaptersLoader.getPagesCount(chapter.id) else 0,
currentPage = state?.page ?: 0,
isSliderEnabled = settings.isReaderSliderEnabled,
@@ -398,7 +391,7 @@ class ReaderViewModel @Inject constructor(
private fun computePercent(chapterId: Long, pageIndex: Int): Float {
val branch = chaptersLoader.peekChapter(chapterId)?.branch
val chapters = manga?.any?.getChapters(branch) ?: return PROGRESS_NONE
val chapters = manga?.chapters?.get(branch) ?: return PROGRESS_NONE
val chaptersCount = chapters.size
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }
val pagesCount = chaptersLoader.getPagesCount(chapterId)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.colorfilter
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
@@ -121,10 +122,10 @@ class ColorFilterConfigActivity :
.scale(Scale.FILL)
.decodeRegion()
.tag(page.source)
.bitmapConfig(if (viewModel.is32BitColorsEnabled) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565)
.indicator(listOf(viewBinding.progressBefore, viewBinding.progressAfter))
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(viewBinding.imageViewBefore))
.allowRgb565(false)
.target(DoubleViewTarget(viewBinding.imageViewBefore, viewBinding.imageViewAfter))
.enqueueWith(coil)
}

View File

@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPage
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
@@ -18,6 +19,7 @@ import javax.inject.Inject
@HiltViewModel
class ColorFilterConfigViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val settings: AppSettings,
private val mangaDataRepository: MangaDataRepository,
) : BaseViewModel() {
@@ -31,6 +33,9 @@ class ColorFilterConfigViewModel @Inject constructor(
val isChanged: Boolean
get() = colorFilter.value != initialColorFilter
val is32BitColorsEnabled: Boolean
get() = settings.is32BitColorsEnabled
init {
launchLoadingJob {
initialColorFilter = mangaDataRepository.getColorFilter(manga.id)

View File

@@ -118,7 +118,7 @@ class ReaderConfigSheet :
R.id.button_color_filter -> {
val page = viewModel.getCurrentPage() ?: return
val manga = viewModel.manga?.any ?: return
val manga = viewModel.manga?.toManga() ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
}
}

View File

@@ -1,8 +1,14 @@
package org.koitharu.kotatsu.reader.ui.config
import android.content.SharedPreferences
import android.graphics.Bitmap
import android.view.View
import androidx.annotation.CheckResult
import androidx.lifecycle.MediatorLiveData
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -12,6 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
class ReaderSettings(
@@ -29,6 +36,13 @@ class ReaderSettings(
val colorFilter: ReaderColorFilter?
get() = colorFilterFlow.value?.takeUnless { it.isEmpty }
val bitmapConfig: Bitmap.Config
get() = if (settings.is32BitColorsEnabled) {
Bitmap.Config.ARGB_8888
} else {
Bitmap.Config.RGB_565
}
val isPagesNumbersEnabled: Boolean
get() = settings.isPagesNumbersEnabled
@@ -40,6 +54,22 @@ class ReaderSettings(
view.background = bg.resolve(view.context)
}
@CheckResult
fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean {
val config = bitmapConfig
return if (ssiv.regionDecoderFactory.bitmapConfig != config) {
ssiv.regionDecoderFactory = if (ssiv.context.isLowRamDevice()) {
SkiaImageRegionDecoder.Factory(config)
} else {
SkiaPooledImageRegionDecoder.Factory(config)
}
ssiv.bitmapDecoderFactory = SkiaImageDecoder.Factory(config)
true
} else {
false
}
}
override fun onInactive() {
super.onInactive()
settings.unsubscribe(internalObserver)
@@ -78,7 +108,8 @@ class ReaderSettings(
key == AppSettings.KEY_PAGES_NUMBERS ||
key == AppSettings.KEY_WEBTOON_ZOOM ||
key == AppSettings.KEY_READER_ZOOM_BUTTONS ||
key == AppSettings.KEY_READER_BACKGROUND
key == AppSettings.KEY_READER_BACKGROUND ||
key == AppSettings.KEY_32BIT_COLOR
) {
notifyChanged()
}

View File

@@ -84,6 +84,14 @@ class PageHolderDelegate(
job?.cancel()
}
fun reload() {
if (state == State.SHOWN ) {
file?.let {
callback.onImageReady(it.toUri())
}
}
}
override fun onReady() {
state = State.SHOWING
error = null

View File

@@ -46,6 +46,10 @@ open class PageHolder(
override fun onConfigChanged() {
super.onConfigChanged()
binding.zoomControl.isVisible = settings.isZoomControlsEnabled
@Suppress("SENSELESS_COMPARISON")
if (settings.applyBitmapConfig(binding.ssiv) && delegate != null) {
delegate.reload()
}
}
@SuppressLint("SetTextI18n")

View File

@@ -6,7 +6,6 @@ import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkState
@@ -34,12 +33,19 @@ class WebtoonHolder(
init {
binding.ssiv.bindToLifecycle(owner)
binding.ssiv.regionDecoderFactory = SkiaPooledImageRegionDecoder.Factory()
binding.ssiv.addOnImageEventListener(delegate)
bindingInfo.buttonRetry.setOnClickListener(this)
bindingInfo.buttonErrorDetails.setOnClickListener(this)
}
override fun onConfigChanged() {
super.onConfigChanged()
@Suppress("SENSELESS_COMPARISON")
if (settings.applyBitmapConfig(binding.ssiv) && delegate != null) {
delegate.reload()
}
}
override fun onBind(data: ReaderPage) {
delegate.onBind(data.toMangaPage())
}

View File

@@ -7,17 +7,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.firstNotNull
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.domain.ChaptersLoader
@@ -28,7 +28,7 @@ class PagesThumbnailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
private val chaptersLoader: ChaptersLoader,
doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
detailsLoadUseCase: DetailsLoadUseCase,
) : BaseViewModel() {
private val currentPageIndex: Int =
@@ -37,7 +37,7 @@ class PagesThumbnailsViewModel @Inject constructor(
val manga = savedStateHandle.require<ParcelableManga>(PagesThumbnailsSheet.ARG_MANGA).manga
private val repository = mangaRepositoryFactory.create(manga.source)
private val mangaDetails = doubleMangaLoadUseCase(manga).map {
private val mangaDetails = detailsLoadUseCase(MangaIntent.of(manga)).map {
val b = manga.chapters?.findById(initialChapterId)?.branch
branch.value = b
it.filterChapters(b)
@@ -52,8 +52,7 @@ class PagesThumbnailsViewModel @Inject constructor(
init {
loadingJob = launchLoadingJob(Dispatchers.Default) {
chaptersLoader.init(viewModelScope, mangaDetails.filterNotNull())
mangaDetails.first { x -> x?.hasChapter(initialChapterId) == true }
chaptersLoader.init(checkNotNull(mangaDetails.first { x -> x?.isLoaded == true }))
chaptersLoader.loadSingleChapter(initialChapterId)
updateList()
}
@@ -79,13 +78,13 @@ class PagesThumbnailsViewModel @Inject constructor(
updateList()
}
private suspend fun updateList() {
private fun updateList() {
val snapshot = chaptersLoader.snapshot()
val pages = buildList(snapshot.size + chaptersLoader.size + 2) {
var previousChapterId = 0L
for (page in snapshot) {
if (page.chapterId != previousChapterId) {
chaptersLoader.awaitChapter(page.chapterId)?.let {
chaptersLoader.peekChapter(page.chapterId)?.let {
add(ListHeader(it.name))
}
previousChapterId = page.chapterId

View File

@@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
@@ -67,7 +68,7 @@ open class RemoteListViewModel @Inject constructor(
private var randomJob: Job? = null
override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() },
mangaList.map { it?.distinctById()?.skipNsfwIfNeeded() },
listMode,
listError,
hasNextPage,
@@ -138,7 +139,7 @@ open class RemoteListViewModel @Inject constructor(
} else if (list.isNotEmpty()) {
mangaList.value = mangaList.value?.plus(list) ?: list
}
hasNextPage.value = list.isNotEmpty()
hasNextPage.value = list.isNotEmpty() // TODO check if new ids added
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.domain.ListExtraProvider
@@ -31,6 +32,9 @@ class SuggestionsViewModel @Inject constructor(
private val suggestionsScheduler: SuggestionsWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) {
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_SUGGESTIONS) { suggestionsListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.suggestionsListMode)
override val content = combine(
repository.observeAll(),
listMode,

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.6" android:color="?android:colorBackground" />
</selector>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.6" android:color="@android:color/black" />
</selector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Fills the entire area with the divider's color first... -->
<item>
<shape
android:shape="rectangle">
<solid android:color="?attr/colorOutline"/>
</shape>
</item>
<!-- ..., then draws a rectangle with the container color to cover the area not for the divider. -->
<item
android:bottom="1dp">
<shape
android:shape="rectangle">
<solid android:color="?attr/m3ColorBackground"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M5 5V19H7V21H3V3H7V5H5M20 7H7V9H20V7M20 11H7V13H20V11M20 15H7V17H20V15Z" />
</vector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Fills the entire area with the divider's color first... -->
<item>
<shape
android:shape="rectangle">
<solid android:color="@color/kotatsu_primaryContainer"/>
</shape>
</item>
<!-- ..., then draws a rectangle with the container color to cover the area not for the divider. -->
<item
android:bottom="1dp">
<shape
android:shape="rectangle">
<solid android:color="@color/kotatsu_m3_background"/>
</shape>
</item>
</layer-list>

View File

@@ -21,7 +21,6 @@
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/toolbar_background"
android:theme="?attr/actionBarTheme"
app:layout_scrollFlags="noScroll"
tools:ignore="PrivateResource" />

View File

@@ -12,11 +12,13 @@
android:id="@+id/headerBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/options" />
app:title="@string/list_options" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:scrollIndicators="top"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
@@ -97,6 +99,56 @@
tools:value="100"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_order_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/sort_order"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_order"
style="?materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:visibility="gone"
app:shapeAppearance="?shapeAppearanceCornerMedium"
app:strokeColor="@color/m3_button_outline_color_selector"
app:strokeWidth="@dimen/m3_comp_outlined_button_outline_width"
tools:visibility="visible">
<Spinner
android:id="@+id/spinner_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?listPreferredItemHeightSmall" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_grouping"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:layout_marginTop="@dimen/margin_normal"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:ellipsize="end"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:singleLine="true"
android:text="@string/group"
android:textAppearance="?attr/textAppearanceButton"
android:textColor="?colorOnSurfaceVariant"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_list_group"
tools:visibility="visible" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -2,22 +2,9 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_order"
android:orderInCategory="50"
android:title="@string/sort_order">
<menu>
<group android:id="@+id/group_order" />
</menu>
</item>
<item
android:id="@+id/action_edit"
android:orderInCategory="50"
android:orderInCategory="20"
android:title="@string/edit_category"
android:titleCondensed="@string/edit" />

View File

@@ -5,7 +5,7 @@
<item
android:id="@+id/action_manage"
android:orderInCategory="48"
android:title="@string/manage_categories"
android:titleCondensed="@string/manage" />
android:title="@string/favourites_categories"
android:titleCondensed="@string/categories" />
</menu>

View File

@@ -3,27 +3,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_order"
android:orderInCategory="25"
android:title="@string/sort_order">
<menu>
<group android:id="@+id/group_order" />
</menu>
</item>
<item
android:id="@+id/action_history_grouping"
android:checkable="true"
android:checked="true"
android:orderInCategory="25"
android:title="@string/group"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_history"
android:orderInCategory="10"

View File

@@ -5,8 +5,8 @@
<item
android:id="@+id/action_list_mode"
android:orderInCategory="20"
android:title="@string/list_mode"
android:orderInCategory="40"
android:title="@string/list_options"
app:showAsAction="never" />
</menu>

View File

@@ -485,4 +485,12 @@
<string name="zoom_out">Зменшыць</string>
<string name="keep_screen_on">Трымаць экран уключаным</string>
<string name="keep_screen_on_summary">Ня выключаць экран падчас чытання мангі</string>
<string name="state_abandoned">Кінута</string>
<string name="categories">Катэгорыі</string>
<string name="list_options">Спіс опцый</string>
<string name="suggest_new_sources">Прапаноўваць новыя крыніцы пасля абнаўлення праграмы</string>
<string name="enhanced_colors_summary">Памяншае паласы, але можа паўплываць на прадукцыйнасць</string>
<string name="by_relevance">Актуальнасць</string>
<string name="enhanced_colors">32-бітны каляровы рэжым</string>
<string name="suggest_new_sources_summary">Запыт на ўключэнне зноў дададзеных крыніц пасля абнаўлення праграмы</string>
</resources>

View File

@@ -65,7 +65,7 @@
<string name="text_empty_holder_primary">Je tu nějak prázdno…</string>
<string name="text_search_holder_secondary">Zkuste přeformulovat dotaz.</string>
<string name="text_history_holder_primary">To co čtete se zobrazí zde</string>
<string name="text_history_holder_secondary">Zjistěte co číst na boční nabídce.</string>
<string name="text_history_holder_secondary">Co si přečíst, najdete v sekci «Prozkoumat»</string>
<string name="text_shelf_holder_primary">Vaše manga bude zobrazena zde</string>
<string name="text_shelf_holder_secondary">Zjistěte co číst v sekci «Prozkoumat»</string>
<string name="text_local_holder_primary">Nejdříve něco uložte</string>
@@ -283,7 +283,7 @@
<string name="language">Jazyk</string>
<string name="share_logs">Sdílet záznamy</string>
<string name="enable_logging">Zapnout zaznamenávání</string>
<string name="enable_logging_summary">Zaznamenat některé akce pro spravovací účely</string>
<string name="enable_logging_summary">Zaznamenejte některé akce pro účely ladění. Nezapínejte jej, pokud si nejste jisti, co děláte</string>
<string name="show_suspicious_content">Zobrazovat podezřelý kontent</string>
<string name="theme_name_dynamic">Dynamické</string>
<string name="color_theme">Schéma barev</string>
@@ -299,7 +299,7 @@
<string name="scrobbling_empty_hint">Pro sledování pokroku čtení, vyberte Menu → Sledovat na displeji detailů mangy.</string>
<string name="services">Služby</string>
<string name="allow_unstable_updates">Povolit nestabilní aktualizace</string>
<string name="allow_unstable_updates_summary">Navrhnout beta aktualizace této aplikace</string>
<string name="allow_unstable_updates_summary">Přijímat oznámení o nestabilních sestaveních</string>
<string name="download_started">Stahování začalo</string>
<string name="got_it">Mám to</string>
<string name="sources_reorder_tip">Klikněte a přidržte na předmětu pro přeskupení</string>
@@ -314,7 +314,7 @@
<string name="server_address">Adresa serveru</string>
<string name="ignore_ssl_errors">Ignorovat SSL chyby</string>
<string name="mirror_switching">Vybrat zrcadlo automaticky</string>
<string name="mirror_switching_summary">Automaticky prohodit doménu pro vzdálené zdroje při chybě pokud jsou zrcadla dostupná</string>
<string name="mirror_switching_summary">Automaticky přepínat domény pro zdroje manga při chybách, pokud jsou k dispozici zrcadla</string>
<string name="pause">Pozastavit</string>
<string name="resume">Vrátit</string>
<string name="paused">Pozastaveno</string>
@@ -443,4 +443,5 @@
<string name="webtoon_zoom_summary">Povolit přiblížení v gestu ve webtoon režimu</string>
<string name="clear_source_cookies_summary">Vyčistit cookies pouze pro specifikované domény. Ve většině případech bude neplatná autorizace</string>
<string name="download_option_manual_selection">Vyberte kapitoly manuálně</string>
<string name="description">Popis</string>
</resources>

View File

@@ -47,7 +47,7 @@
<string name="dark">Oscuro</string>
<string name="automatic">De acuerdo al sistema</string>
<string name="pages">Páginas</string>
<string name="clear">Borrar</string>
<string name="clear">Limpiar</string>
<string name="text_clear_history_prompt">Borrar todo el historial de lectura de forma permanente\?</string>
<string name="remove">Eliminar</string>
<string name="_s_deleted_from_local_storage">«%s» borrado del almacenamiento local</string>
@@ -485,4 +485,12 @@
<string name="zoom_out">Alejar</string>
<string name="keep_screen_on">Mantener pantalla encendida</string>
<string name="keep_screen_on_summary">No apagues la pantalla mientras lees manga</string>
<string name="state_abandoned">Abandonó</string>
<string name="suggest_new_sources">Sugerir nuevas fuentes tras actualizar la aplicación</string>
<string name="enhanced_colors">Modo de color de 32 bits</string>
<string name="suggest_new_sources_summary">Solicitud para habilitar las nuevas fuentes añadidas tras actualizar la aplicación</string>
<string name="categories">Categorías</string>
<string name="list_options">Lista de opciones</string>
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
<string name="by_relevance">Relevancia</string>
</resources>

View File

@@ -483,4 +483,14 @@
<string name="main_screen_sections">Mga pangunahing seksyon ng screen</string>
<string name="zoom_out">Mag-zoom palabas</string>
<string name="to_top">Sa taas</string>
<string name="suggest_new_sources">Magmungkahi ng mga bagong source pagkatapos ng app update</string>
<string name="enhanced_colors_summary">Binabawasan ang banding, ngunit maaaring makaapekto ito sa performance</string>
<string name="state_abandoned">Na-drop</string>
<string name="keep_screen_on">Panatilihing naka-on ang screen</string>
<string name="enhanced_colors">32-bit color mode</string>
<string name="keep_screen_on_summary">Huwag I-off ang screen habang nagbabasa ng manga</string>
<string name="suggest_new_sources_summary">I-prompt na paganahin ang mga bagong idinagdag na source pagkatapos i-update ang aplikasyon</string>
<string name="categories">Mga Kategorya</string>
<string name="list_options">Opsyon sa Listahan</string>
<string name="by_relevance">Kaugnayan</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="details">विवरण</string>
<string name="chapters">अध्याय</string>
<string name="nothing_found">कुछ नहीं मिला</string>
@@ -64,4 +64,66 @@
<string name="automatic">फोन जैसा</string>
<string name="pages">पन्ने</string>
<string name="no_description">कोई विवरण नहीं है</string>
<string name="updates">अपडेट</string>
<string name="manga_shelf">शेल्फ</string>
<string name="text_history_holder_secondary">«अन्वेषण» विभाग में जो भी आपको पढ़ना है उसे खोजे</string>
<string name="all_favourites">सर्वे प्रिय</string>
<string name="light_indicator">LED इंडिकेटर</string>
<string name="favourites_categories">पसंदिता केटेगरी</string>
<string name="gestures_only">केवल जेस्चर</string>
<string name="clear_thumbs_cache">थंबनेल कैच को साफ करे</string>
<string name="taps_on_edges">किनारे पर टैप</string>
<string name="switch_pages">पेज को बदले</string>
<string name="rotate_screen">स्क्रीन गुमाए</string>
<string name="text_shelf_holder_secondary">«अन्वेषण» विभाग में जो भी पढ़ना है उसे खोजे</string>
<string name="vibration">वैब्रेशन</string>
<string name="remove_category">निकालो</string>
<string name="read_mode">पढ़ने की विधि</string>
<string name="internal_storage">आंतरिक स्टोरेज</string>
<string name="read_later">बाद में पढ़े</string>
<string name="cannot_find_available_storage">स्टोरेज उपलब्ध नहीं हैं</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%2$d में से %1$d</string>
<string name="text_feed_holder">जो भी मांगा आप पढ़ रहे हो उसके नए चैप्टर यहां दिखाएं देंगे</string>
<string name="favourites_category_empty">केटेगोरी खाली हैं</string>
<string name="manga_save_location">डाउनलोड करने का फोल्डर</string>
<string name="updates_feed_cleared">साफ हो गया</string>
<string name="update">अपडेट</string>
<string name="feed_will_update_soon">फीड अपडेट शीघ्र ही आरंभ होगा</string>
<string name="app_update_available">इस ऐप का नया संस्करण उपलब्ध हैं</string>
<string name="new_version_s">नया संस्करण: %s</string>
<string name="text_delete_local_manga">डिवाइस में से %s ko सदा के लिए निकाल दे\?</string>
<string name="text_history_holder_primary">जो भी आप पढ़ोगे वे सब यहां दिखेगा</string>
<string name="delete_manga">मांगा को नष्ट करे</string>
<string name="notification_sound">सूचना की ध्वनि</string>
<string name="search_history_cleared">साफ हो गया</string>
<string name="open_in_browser">ब्राउसर में खोले</string>
<string name="notifications">सूचनाएं</string>
<string name="not_available">उपल्ब्ध नहीं हैं</string>
<string name="track_sources">अपडेट के लिए देखे</string>
<string name="clear_search_history">खोजा हुवा इतिहास को साफ करे</string>
<string name="download">डाउनलोड</string>
<string name="size_s">साइज: %s</string>
<string name="text_shelf_holder_primary">आपके मांगा यहाँ दिखाई देंगे</string>
<string name="new_chapters">नए चैप्टर</string>
<string name="volume_buttons">वॉल्यूम बटन</string>
<string name="clear_updates_feed">अपडेट्स फीड को साफ करे</string>
<string name="notifications_settings">सुचना के सेटिंग</string>
<string name="domain">क्षेत्र</string>
<string name="save_manga">जमा करो</string>
<string name="large_manga_save_confirm">इस मांगा में %s हैं। सबको जमा करे\?</string>
<string name="reader_settings">रीडर के सेटिंग</string>
<string name="text_search_holder_secondary">क्वेरी को पुनः बनाने का प्रयास करें।</string>
<string name="error">त्रुटि</string>
<string name="grid_size">ग्रिड का आकार</string>
<string name="_continue">जारी रखें</string>
<string name="recent_manga">अभी के</string>
<string name="search_on_s">%s पर खोजो</string>
<string name="search_results">खोज के परिणाम</string>
<string name="pages_animation">पेज का एनीमेशन</string>
<string name="other_storage">अन्य स्टोरेज</string>
<string name="external_storage">बाहरी स्टोरेज</string>
<string name="text_local_holder_secondary">ऑनलाइन सूत्रों से जामा करे अथवा फाईल को आयात करे।</string>
<string name="text_local_holder_primary">पहले कुछ जमा करे</string>
<string name="text_empty_holder_primary">लगता है यहां तोह कुछ नहीं हैं…</string>
<string name="done">होगाया</string>
</resources>

View File

@@ -477,4 +477,5 @@
<string name="items_limit_exceeded">Tidak ada lagi item yang bisa ditambahkan</string>
<string name="directories">Direktori</string>
<string name="main_screen_sections">Bagian layar utama</string>
<string name="to_top">Ke atas</string>
</resources>

View File

@@ -1,27 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="new_chapters">
<item quantity="one">%1$d nytt kapittel</item>
<item quantity="other">%1$d nye kapitler</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d kapittel</item>
<item quantity="other">%1$d kapitler</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d time siden</item>
<item quantity="other">%1$d timer siden</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d dag siden</item>
<item quantity="other">%1$d dager siden</item>
</plurals>
<plurals name="items">
<item quantity="one">%1$d element</item>
<item quantity="other">%1$d elementer</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minutt siden</item>
<item quantity="other">%1$d minutter siden</item>
</plurals>
</resources>
<plurals name="new_chapters">
<item quantity="one">%1$d nytt kapittel</item>
<item quantity="other">%1$d nye kapitler</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d kapittel</item>
<item quantity="other">%1$d kapitler</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d time siden</item>
<item quantity="other">%1$d timer siden</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d dag siden</item>
<item quantity="other">%1$d dager siden</item>
</plurals>
<plurals name="items">
<item quantity="one">%1$d element</item>
<item quantity="other">%1$d elementer</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minutt siden</item>
<item quantity="other">%1$d minutter siden</item>
</plurals>
<plurals name="months_ago">
<item quantity="one">%1$d måned siden</item>
<item quantity="other">%1$d måneder siden</item>
</plurals>
</resources>

View File

@@ -38,6 +38,7 @@
<item name="colorOutlineVariant">#3F4947</item>
<item name="m3ColorBackground">@color/background_miku</item>
<item name="m3ColorExploreButton">#1D2020</item>
<item name="m3ColorCardBackground">#282C2C</item>
<item name="m3ColorBottomMenuBackground">#272B2A</item>
</style>
@@ -78,6 +79,7 @@
<item name="colorOutlineVariant">#534342</item>
<item name="m3ColorBackground">@color/background_asuka</item>
<item name="m3ColorExploreButton">#251E1E</item>
<item name="m3ColorCardBackground">#2E2525</item>
<item name="m3ColorBottomMenuBackground">#2F2828</item>
</style>
@@ -118,6 +120,7 @@
<item name="colorOutlineVariant">#404943</item>
<item name="m3ColorBackground">@color/background_mion</item>
<item name="m3ColorExploreButton">#1D201E</item>
<item name="m3ColorCardBackground">#262B28</item>
<item name="m3ColorBottomMenuBackground">#272B28</item>
</style>
@@ -158,6 +161,7 @@
<item name="colorOutlineVariant">#524343</item>
<item name="m3ColorBackground">@color/background_rikka</item>
<item name="m3ColorExploreButton">#241E1E</item>
<item name="m3ColorCardBackground">#302828</item>
<item name="m3ColorBottomMenuBackground">#2F2828</item>
</style>
@@ -198,6 +202,7 @@
<item name="colorOutlineVariant">#40484B</item>
<item name="m3ColorBackground">@color/background_sakura</item>
<item name="m3ColorExploreButton">#1D2021</item>
<item name="m3ColorCardBackground">#25292B</item>
<item name="m3ColorBottomMenuBackground">#272A2C</item>
</style>
@@ -238,6 +243,7 @@
<item name="colorOutlineVariant">#524344</item>
<item name="m3ColorBackground">@color/background_mamimi</item>
<item name="m3ColorExploreButton">#241E1E</item>
<item name="m3ColorCardBackground">#2B2323</item>
<item name="m3ColorBottomMenuBackground">#2F2828</item>
</style>
@@ -278,6 +284,7 @@
<item name="colorOutlineVariant">#494949</item>
<item name="m3ColorBackground">@color/background_kanade</item>
<item name="m3ColorExploreButton">#292929</item>
<item name="m3ColorCardBackground">#303030</item>
<item name="m3ColorBottomMenuBackground">#2C2C2C</item>
</style>
</resources>

View File

@@ -57,6 +57,7 @@
<item name="android:colorBackground">@android:color/system_neutral2_900</item>
<item name="m3ColorBackground">@android:color/system_neutral2_900</item>
<item name="m3ColorExploreButton">@android:color/system_neutral2_800</item>
<item name="m3ColorCardBackground">@android:color/system_neutral2_700</item>
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_800</item>
<!-- Default Framework Text Colors. -->
<item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text

View File

@@ -43,5 +43,6 @@
<color name="kotatsu_scrim">#000000</color>
<color name="kotatsu_m3_background">#1A1B1F</color>
<color name="kotatsu_m3_exploreButton">#292A2D</color>
<color name="kotatsu_m3_cardBackground">#313235</color>
<color name="kotatsu_m3_bottom_menu">#2F3033</color>
</resources>

View File

@@ -8,6 +8,7 @@
<item name="android:colorBackground">@color/background_amoled</item>
<item name="m3ColorBackground">@color/background_amoled</item>
<item name="m3ColorExploreButton">@color/surface_amoled</item>
<item name="m3ColorCardBackground">@color/surface_amoled</item>
<item name="m3ColorBottomMenuBackground">@color/surface_amoled</item>
</style>

View File

@@ -485,4 +485,12 @@
<string name="zoom_out">Отдалить</string>
<string name="keep_screen_on">Держать экран включённым</string>
<string name="keep_screen_on_summary">Не выключать экран во время чтения манги</string>
<string name="state_abandoned">Заброшена</string>
<string name="categories">Категории</string>
<string name="list_options">Параметры списка</string>
<string name="suggest_new_sources">Предлагать новые источники после обновления приложения</string>
<string name="enhanced_colors_summary">Уменьшает полосы, но может повлиять на производительность</string>
<string name="by_relevance">Актуальность</string>
<string name="enhanced_colors">32-битный цветовой режим</string>
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
</resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="local_storage">Локално складиште</string>
<string name="error_occurred">Грешка се појавила</string>
<string name="favourites">Фаворити</string>
@@ -126,4 +126,25 @@
<string name="show_on_shelf">Прикажи на полици</string>
<string name="explore">Преглед</string>
<string name="options">Опције</string>
<string name="add_to_favourites">Додај у фаворите</string>
<string name="text_history_holder_secondary">Пронађите ствари за читање у одељку „Преглед“</string>
<string name="light_indicator">ЛЕД индикатор</string>
<string name="favourites_categories">Омиљене категорије</string>
<string name="remove_category">Избриши</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">Омогућено је %1$d од %2$d</string>
<string name="clear">Поспремити</string>
<string name="text_history_holder_primary">Оно што прочитате биће приказано овде</string>
<string name="delete">Избриши</string>
<string name="search_history_cleared">Очишћено</string>
<string name="_s_deleted_from_local_storage">\"%s\" избрисано из локалне меморије</string>
<string name="_import">Увоз</string>
<string name="download">Преузимање</string>
<string name="text_shelf_holder_primary">Ваша манга ће бити приказана овде</string>
<string name="share_s">Подели %s</string>
<string name="domain">Домаин</string>
<string name="save_manga">Сачувати</string>
<string name="large_manga_save_confirm">Ова манга има %s. Сачувати све\?</string>
<string name="text_search_holder_secondary">Покушајте да преформулишете захтев.</string>
<string name="_continue">Настави</string>
<string name="text_local_holder_primary">Прво сачувајте нешто</string>
</resources>

View File

@@ -301,4 +301,47 @@
<string name="show_all">แสดงทั้งหมด</string>
<string name="empty_favourite_categories">ไม่มีหมวดหมู่ที่ชื่นชอบ</string>
<string name="invalid_domain_message">โดเมนไม่ถูกต้อง</string>
<string name="tracking">การติดตาม</string>
<string name="cancel_all">ยกเลิกทั้งหมด</string>
<string name="reset">รีเซ็ต</string>
<string name="gestures_only">ท่าทางเท่านั้น</string>
<string name="source_disabled">แหล่งที่มาถูกปิดใช้งาน</string>
<string name="storage_usage">การใช้พื้นที่เก็บข้อมูล</string>
<string name="show_notification_new_chapters_on">คุณจะได้รับการแจ้งเตือนเกี่ยวกับการอัปเดตมังงะที่คุณกำลังอ่าน</string>
<string name="various_languages">ภาษาต่างๆ</string>
<string name="color_correction_hint">การตั้งค่าสีที่เลือกจะถูกจดจำไว้สำหรับมังงะเรื่องนี้</string>
<string name="not_found_404">ไม่พบหรือเนื้อหาอาจถูกลบแล้ว</string>
<string name="feed_will_update_soon">การอัปเดตฟีดกำลังจะเริ่มเร็วๆ นี้</string>
<string name="server_error">ข้อผิดพลาดฝั่งเซิร์ฟเวอร์ (%1$d) กรุณาลองใหม่อีกครั้งในภายหลัง</string>
<string name="network_unavailable_hint">เปิด Wi-Fi หรือเครือข่ายมือถือเพื่ออ่านมังงะออนไลน์</string>
<string name="confirm_exit">กดย้อนกลับอีกครั้งเพื่อออก</string>
<string name="sync_settings">การตั้งค่าการซิงค์ข้อมูล</string>
<string name="other_cache">แคชอื่นๆ</string>
<string name="translations">การแปล</string>
<string name="downloads_wifi_only">ดาวน์โหลดผ่าน Wi-Fi เท่านั้น</string>
<string name="discard">ทิ้ง</string>
<string name="saved_manga">มังงะที่บันทึกไว้</string>
<string name="automatic_scroll">เลื่อนอัตโนมัติ</string>
<string name="invalid_port_number">เลขพอร์ตไม่ถูกต้อง</string>
<string name="suggestions_info">ข้อมูลทั้งหมดจะได้รับการวิเคราะห์ในอุปกรณ์นี้เท่านั้นและไม่เคยส่งไปที่ไหน</string>
<string name="contrast">คอนทราสต์</string>
<string name="options">ตัวเลือก</string>
<string name="download_slowdown">ดาวน์โหลดช้าลง</string>
<string name="sync">การซิงค์ข้อมูล</string>
<string name="random">สุ่ม</string>
<string name="clear_updates_feed">เคลียร์ฟีดอัปเดต</string>
<string name="import_completed_hint">คุณสามารถลบไฟล์ต้นฉบับออกจากที่เก็บข้อมูลเพื่อประหยัดพื้นที่</string>
<string name="language">ภาษา</string>
<string name="incognito_mode">โหมดไม่ระบุตัวตน</string>
<string name="no_bookmarks_summary">คุณสามารถสร้างบุ๊คมาร์กขณะอ่านมังงะได้</string>
<string name="available">คงเหลือ</string>
<string name="network_unavailable">เครือข่ายไม่พร้อมใช้งาน</string>
<string name="empty">ว่างเปล่า</string>
<string name="text_unsaved_changes_prompt">บันทึกหรือละทิ้งการเปลี่ยนแปลงที่ยังไม่ได้บันทึก\?</string>
<string name="color_theme">โทนสี</string>
<string name="brightness">ความสว่าง</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="exit_confirmation_summary">กดย้อนกลับสองครั้งเพื่อออกจากแอป</string>
<string name="bookmarks_removed">ลบบุ๊คมาร์กแล้ว</string>
<string name="error_no_space_left">ไม่มีพื้นที่เหลือบนอุปกรณ์แล้ว</string>
</resources>

View File

@@ -1,27 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="items">
<item quantity="one">%1$d öge</item>
<item quantity="other">%1$d öge</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d bölüm</item>
<item quantity="other">%1$d bölüm</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d saat önce</item>
<item quantity="other">%1$d saat önce</item>
</plurals>
<plurals name="new_chapters">
<item quantity="one">%1$d yeni bölüm</item>
<item quantity="other">%1$d yeni bölüm</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d gün önce</item>
<item quantity="other">%1$d gün önce</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d dakika önce</item>
<item quantity="other">%1$d dakika önce</item>
</plurals>
</resources>
<plurals name="items">
<item quantity="one">%1$d öge</item>
<item quantity="other">%1$d öge</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d bölüm</item>
<item quantity="other">%1$d bölüm</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d saat önce</item>
<item quantity="other">%1$d saat önce</item>
</plurals>
<plurals name="new_chapters">
<item quantity="one">%1$d yeni bölüm</item>
<item quantity="other">%1$d yeni bölüm</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d gün önce</item>
<item quantity="other">%1$d gün önce</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d dakika önce</item>
<item quantity="other">%1$d dakika önce</item>
</plurals>
<plurals name="months_ago">
<item quantity="one">%1$d ay önce</item>
<item quantity="other"></item>
</plurals>
</resources>

View File

@@ -123,7 +123,7 @@
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d / %2$d açık</string>
<string name="text_search_holder_secondary">Sorguyu yeniden biçimlendirmeyi deneyin.</string>
<string name="text_history_holder_primary">Okuduklarınız burada görüntülenecek</string>
<string name="text_history_holder_secondary">Yan menüde ne okuyacağınızı bulun.</string>
<string name="text_history_holder_secondary">Keşfet menüsünde ne okuyacağınızı bulun.</string>
<string name="text_local_holder_primary">Önce bir şey kaydedin</string>
<string name="text_local_holder_secondary">Çevrim içi kaynaklardan kaydedin veya dosyaları içe aktarın.</string>
<string name="manga_shelf">Raf</string>
@@ -351,7 +351,7 @@
<string name="language">Dil</string>
<string name="share_logs">Günlükleri paylaş</string>
<string name="enable_logging">Günlük kaydını etkinleştir</string>
<string name="enable_logging_summary">Hata ayıklama amacıyla bazı eylemleri kaydedin</string>
<string name="enable_logging_summary">Hata ayıklama amacıyla bazı eylemleri kaydedin. Ne yaptığınızdan emin değilseniz açmayın</string>
<string name="show_suspicious_content">Şüpheli içeriği göster</string>
<string name="services">Hizmetler</string>
<string name="scrobbling_empty_hint">Okuma ilerlemesini izlemek için manga ayrıntıları ekranında Menü → İzle\'yi seçin.</string>
@@ -362,7 +362,7 @@
<string name="theme_name_mamimi">Mamimi</string>
<string name="theme_name_kanade">Kanade</string>
<string name="user_agent">UserAgent başlığı</string>
<string name="allow_unstable_updates_summary">Uygulamanın beta sürümleri için güncellemeler öner</string>
<string name="allow_unstable_updates_summary">Kararsız yapılar hakkında bildirimler alın</string>
<string name="allow_unstable_updates">Kararsız güncellemelere izin ver</string>
<string name="download_started">İndirme başladı</string>
<string name="theme_name_miku">Miku</string>
@@ -380,7 +380,7 @@
<string name="sync_settings">Eşitleme seçenekleri</string>
<string name="server_address">Sunucu adresi</string>
<string name="sync_host_description">Şirket içinde barındırılan bir eşitleme sunucusu veya varsayılan bir sunucu kullanabilirsiniz. Ne yaptığınızdan emin değilseniz bunu değiştirmeyin.</string>
<string name="mirror_switching_summary">Yansıtmalar varsa, hatalarda uzak kaynaklar için etki alanlarını otomatik olarak değiştir</string>
<string name="mirror_switching_summary">Aynalar mevcutsa hatalarda manga kaynakları için etki alanlarını otomatik olarak değiştir</string>
<string name="downloads_wifi_only_summary">Mobil ağa geçerken indirmeyi durdur</string>
<string name="remove_completed">Bitirilenleri kaldır</string>
<string name="cancel_all">Hepsini iptal et</string>
@@ -431,4 +431,63 @@
<string name="show_pages_numbers_summary">Sayfa numaralarını alt köşede göster</string>
<string name="pages_animation_summary">Sayfa Çevirme Animasyonu</string>
<string name="details_button_tip">Daha fazla seçenek görmek için Oku düğmesini basılı tutun</string>
<string name="languages">Diller</string>
<string name="zoom_in">Yakınlaştır</string>
<string name="captcha_required_summary">Düzgün okunmak için %s captcha gerektiriyor</string>
<string name="download_option_all_unread">Tüm okunmamış bölümler</string>
<string name="progress">İlerleme</string>
<string name="error_corrupted_file">Geçersiz veri döndürülüyor veya dosya bozuk</string>
<string name="pick_custom_directory">Özel dizini seçin</string>
<string name="related_manga_summary">İlgili mangaların bir listesini göster. Bazı durumlarda yanlış veya eksik olabilir</string>
<string name="reader_zoom_buttons_summary">Sağ alt köşede yakınlaştırma kontrol düğmelerini göster veya gösterme</string>
<string name="tracker_wifi_only_summary">Ölçülü ağ bağlantılarını kullanarak yeni bölümleri kontrol etmeyin</string>
<string name="order_added">Eklendi</string>
<string name="on_device">Cihazda</string>
<string name="download_option_whole_manga">Bütün manga</string>
<string name="clear_source_cookies_summary">Yalnızca belirtilen etki alanı için çerezleri temizleyin. Çoğu durumda yetkilendirmeyi geçersiz kılar</string>
<string name="suggest_new_sources">Uygulama güncellemesinden sonra yeni kaynaklar önerin</string>
<string name="moved_to_top">Üste taşındı</string>
<string name="data_not_restored_text">Doğru yedek dosyasını seçtiğinizden emin olun</string>
<string name="view_list">Listeyi gör</string>
<string name="unknown">Bilinmeyen</string>
<string name="in_progress">Devam ediyor</string>
<string name="download_option_manual_selection">Bölümleri elle seç</string>
<string name="enhanced_colors_summary">Bantlanmayı azaltır, ancak performansı etkileyebilir</string>
<string name="items_limit_exceeded">Daha fazla öğe eklenemez</string>
<string name="data_not_restored">Veri geri yüklemedi</string>
<string name="directories">Dizinler</string>
<string name="local_manga_directories">Yerel manga dizinleri</string>
<string name="manage_categories">Kategorileri yönet</string>
<string name="color_light">ık</string>
<string name="search_hint">Manga başlığı girin, tür veya kaynak adı</string>
<string name="description">ıklama</string>
<string name="reader_zoom_buttons">Yakınlaştırma butonlarını göster</string>
<string name="main_screen_sections">Ana ekran bölümleri</string>
<string name="advanced">Gelişmiş</string>
<string name="download_option_all_unread_b">Tüm okunmamış bölümler (%s)</string>
<string name="color_dark">Karanlık</string>
<string name="too_many_requests_message">Çok fazla istek. Daha sonra tekrar deneyin</string>
<string name="related_manga">İlgili manga</string>
<string name="state_abandoned">Bırakıldı</string>
<string name="download_option_first_n_chapters">Önce %s</string>
<string name="keep_screen_on">Ekranıık tut</string>
<string name="suggestions_wifi_only_summary">Ölçülü ağ bağlantılarını kullanarak önerileri güncellemeyin</string>
<string name="custom_directory">Özel dizin</string>
<string name="enhanced_colors">32-bit renk modu</string>
<string name="default_section">Varsayılan bölüm</string>
<string name="background">Arkaplan</string>
<string name="no_access_to_file">Bu dosya veya dizine erişme izniniz yok</string>
<string name="zoom_out">Uzaklaştır</string>
<string name="keep_screen_on_summary">Manga okurken ekranı kapatmayın</string>
<string name="download_option_next_unread_n_chapters">Sonraki okunmamış %s</string>
<string name="voice_search">Sesli arama</string>
<string name="manga_list">Manga listesi</string>
<string name="disable_nsfw">NSFW\'yi devre dışı bırak</string>
<string name="color_white">Beyaz</string>
<string name="to_top">Üste</string>
<string name="show">Göster</string>
<string name="suggest_new_sources_summary">Uygulamayı güncelledikten sonra yeni eklenen kaynakları etkinleştirme istemi</string>
<string name="download_option_all_chapters">Çevirisi %s olan tüm bölümler</string>
<string name="color_black">Siyah</string>
<string name="this_month">Bu ay</string>
</resources>

View File

@@ -485,4 +485,12 @@
<string name="zoom_out">Зменшити</string>
<string name="keep_screen_on">Тримати екран увімкненим</string>
<string name="keep_screen_on_summary">Не вимикати екран під час читання манги</string>
<string name="state_abandoned">Кинута</string>
<string name="categories">Категорії</string>
<string name="list_options">Список опцій</string>
<string name="suggest_new_sources">Пропонуйте нові джерела після оновлення застосунка</string>
<string name="enhanced_colors_summary">Зменшує смуги, але може вплинути на продуктивність</string>
<string name="by_relevance">Актуальність</string>
<string name="enhanced_colors">32-бітний колірний режим</string>
<string name="suggest_new_sources_summary">Запропонувати ввімкнути щойно додані джерела після оновлення застосунка</string>
</resources>

View File

@@ -38,6 +38,7 @@
<item name="colorOutlineVariant">#BEC9C6</item>
<item name="m3ColorBackground">@color/background_miku</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#C4FFF7</item>
</style>
@@ -78,6 +79,7 @@
<item name="colorOutlineVariant">#D8C1C0</item>
<item name="m3ColorBackground">@color/background_asuka</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#FFDAD7</item>
</style>
@@ -118,6 +120,7 @@
<item name="colorOutlineVariant">#C0C9C1</item>
<item name="m3ColorBackground">@color/background_mion</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#CDFFE1</item>
</style>
@@ -158,6 +161,7 @@
<item name="colorOutlineVariant">#C8C5D0</item>
<item name="m3ColorBackground">@color/background_rikka</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#F0EBFF</item>
</style>
@@ -198,6 +202,7 @@
<item name="colorOutlineVariant">#D5C2C6</item>
<item name="m3ColorBackground">@color/background_sakura</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#FFE8ED</item>
</style>
@@ -238,6 +243,7 @@
<item name="colorOutlineVariant">#D7C1C2</item>
<item name="m3ColorBackground">@color/background_mamimi</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#FFE1E2</item>
</style>
@@ -278,6 +284,7 @@
<item name="colorOutlineVariant">#D5D5D5</item>
<item name="m3ColorBackground">@color/background_kanade</item>
<item name="m3ColorExploreButton">#FFFFFF</item>
<item name="m3ColorCardBackground">#FFFFFF</item>
<item name="m3ColorBottomMenuBackground">#DCDCDC</item>
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Base.V23.Kotatsu" parent="Base.Theme.Kotatsu">
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
<style name="Theme.Kotatsu" parent="Base.V23.Kotatsu" />
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Base.V27.Kotatsu" parent="Base.Theme.Kotatsu">
<style name="Base.V27.Kotatsu" parent="Base.V23.Kotatsu">
<item name="android:navigationBarColor">@color/navigation_bar_scrim</item>
<item name="android:windowLightNavigationBar">@bool/light_navigation_bar</item>
</style>

View File

@@ -68,6 +68,7 @@
<item name="android:colorBackground">@android:color/system_neutral2_50</item>
<item name="m3ColorBackground">@android:color/system_neutral2_50</item>
<item name="m3ColorExploreButton">@android:color/white</item>
<item name="m3ColorCardBackground">@android:color/white</item>
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_100</item>
<!-- Default Framework Text Colors. -->
<item name="android:textColorPrimary">@color/m3_dynamic_default_color_primary_text</item>

View File

@@ -428,4 +428,24 @@
<string name="web_view_unavailable">WebView hiện không có sẵn, vui lòng kiểm tra trình cung cấp WebView liệu có được cài đặt</string>
<string name="manga_error_description_pattern">Chi tiết lỗi:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Hãy thử mở truyện trong trình duyệt&lt;/a&gt; để đảm bảo rằng nó có sẵn trên nguồn truyện&lt;br&gt;2. Hãy chắc chắn rằng bạn đang sử dụng &lt;a href=kotatsu://about&gt;phiên bản mới nhất của Kotatsu&lt;/a&gt;&lt;br&gt;3. Nếu có thể, hãy gửi một báo cáo lỗi cho nhà phát triển.</string>
<string name="text_history_holder_secondary">Tìm truyện để đọc trong phần «Khám phá»</string>
<string name="this_month">Tháng này</string>
<string name="tracker_wifi_only_summary">Không kiểm tra các chương mới bằng kết nối mạng có đo lường</string>
<string name="user_agent">Tiêu đề tác nhân người dùng</string>
<string name="data_not_restored_text">Đảm bảo bạn đã chọn đúng tệp sao lưu</string>
<string name="auth_complete">Được ủy quyền</string>
<string name="data_not_restored">Dữ liệu chưa được khôi phục</string>
<string name="manage_categories">Quản lý danh mục</string>
<string name="scrobbling_empty_hint">Để theo dõi tiến trình đọc, chọn Danh sách → Theo dõi trên màn hình chi tiết manga.</string>
<string name="color_light">Sáng</string>
<string name="color_dark">Tối</string>
<string name="allow_unstable_updates_summary">Nhận thông báo về các bản dựng không ổn định</string>
<string name="related_manga">Truyện tranh liên quan</string>
<string name="suggestions_wifi_only_summary">Không cập nhật đề xuất bằng kết nối mạng có đo lường</string>
<string name="comics_archive">Kho lưu trữ truyện tranh</string>
<string name="folder_with_images_import_description">Bạn có thể chọn một thư mục có kho lưu trữ hoặc hình ảnh. Mỗi kho lưu trữ (hoặc thư mục con) sẽ được công nhận là một chương.</string>
<string name="voice_search">Tìm kiếm bằng giọng nói</string>
<string name="reader_control_ltr">Điều khiển trình đọc tiện dụng</string>
<string name="color_white">Trắng</string>
<string name="status_planned">Đã lên kế hoạch</string>
<string name="color_black">Đen</string>
</resources>

View File

@@ -481,4 +481,6 @@
<string name="reader_zoom_buttons_summary">是否在右下角显示缩放按钮</string>
<string name="reader_zoom_buttons">显示缩放按钮</string>
<string name="zoom_out">缩小</string>
<string name="keep_screen_on">保持亮屏</string>
<string name="keep_screen_on_summary">阅读漫画时不关闭屏幕</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More