Compare commits

...

14 Commits
v7.5 ... v7.5.1

Author SHA1 Message Date
Koitharu
c51da5a9d5 Update parsers 2024-09-05 12:52:03 +03:00
Justine Kyle Cobar
bcfce29610 Translated using Weblate (Filipino)
Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: Justine Kyle Cobar <cobarjustinekyle583@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
maryush
a87d18fae3 Translated using Weblate (Polish)
Currently translated at 99.8% (688 of 689 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
gekka
bbd421445c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
Oğuz Ersen
f4e3d797dc Translated using Weblate (Turkish)
Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
Denis Bolba
bd65cbb8b8 Translated using Weblate (Romanian)
Currently translated at 12.9% (89 of 689 strings)

Added translation using Weblate (Romanian)

Added translation using Weblate (Romanian)

Co-authored-by: Denis Bolba <bolbadenis4@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ro/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
Draken
7d1f81607a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
gallegonovato
3b6cd0ea7f Translated using Weblate (Spanish)
Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-09-05 11:03:14 +03:00
Koitharu
aff70d8519 Update dependencies 2024-09-05 10:30:52 +03:00
Koitharu
8a74faa4f0 Fix Downloaded quick filter (close #1076, close #1079) 2024-09-05 10:16:09 +03:00
Koitharu
c1ac207809 Fix downloading (close #1072) 2024-09-04 14:33:08 +03:00
Koitharu
e34e745c84 Fix reading saved manga offline (close #1081, close #1071) 2024-09-04 13:41:57 +03:00
Koitharu
50dd119ab5 Fixes 2024-09-03 17:47:36 +03:00
Koitharu
d0ef177d56 Fix sort order direction in filter 2024-09-03 14:52:04 +03:00
24 changed files with 341 additions and 114 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = 666
versionName = '7.5'
versionCode = 667
versionName = '7.5.1'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -83,23 +83,23 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:b404b44008') {
implementation('com.github.KotatsuApp:kotatsu-parsers:ad726a3fd7') {
exclude group: 'org.json', module: 'json'
}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.0.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC.2'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.1'
implementation 'androidx.fragment:fragment-ktx:1.8.2'
implementation 'androidx.activity:activity-ktx:1.9.2'
implementation 'androidx.fragment:fragment-ktx:1.8.3'
implementation 'androidx.transition:transition-ktx:1.5.1'
implementation 'androidx.collection:collection-ktx:1.4.3'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
implementation 'androidx.lifecycle:lifecycle-service:2.8.4'
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5'
implementation 'androidx.lifecycle:lifecycle-service:2.8.5'
implementation 'androidx.lifecycle:lifecycle-process:2.8.5'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
@@ -107,7 +107,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.4'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.5'
implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.work:work-runtime:2.9.1'

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.ui
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
@@ -100,11 +99,6 @@ abstract class BaseActivity<B : ViewBinding> :
}
override fun onSupportNavigateUp(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// TODO fix behavior on Android 14
dispatchNavigateUp()
return true
}
val fm = supportFragmentManager
if (fm.isStateSaved) {
return false

View File

@@ -5,16 +5,18 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.transformWhile
import org.koitharu.kotatsu.R
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
@@ -101,7 +103,8 @@ fun <T> Flow<T>.withTicker(interval: Long, timeUnit: TimeUnit) = channelFlow<T>
onCompletion { cause ->
close(cause)
}.combine(tickerFlow(interval, timeUnit)) { x, _ -> x }
.collectLatest { send(it) }
.transformWhile<T, Unit> { trySend(it).isSuccess }
.collect()
}
@Suppress("UNCHECKED_CAST")
@@ -127,3 +130,5 @@ fun <T1, T2, T3, T4, T5, T6, R> combine(
suspend fun <T : Any> Flow<T?>.firstNotNull(): T = checkNotNull(first { x -> x != null })
suspend fun <T : Any> Flow<T?>.firstNotNullOrNull(): T? = firstOrNull { x -> x != null }
fun <T> Flow<Flow<T>>.flattenLatest() = flatMapLatest { it }

View File

@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.flattenLatest
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -168,7 +169,7 @@ class MangaSourcesRepository @Inject constructor(
dao.observeEnabled(order).map {
it.toSources(skipNsfw, order)
}
}.flatMapLatest { it }
}.flattenLatest()
.onStart { assimilateNewSources() }
.combine(observeExternalSources()) { enabled, external ->
val list = ArrayList<MangaSourceInfo>(enabled.size + external.size)

View File

@@ -27,6 +27,7 @@ import javax.inject.Inject
@Reusable
class FavouritesRepository @Inject constructor(
private val db: MangaDatabase,
private val localObserver: LocalFavoritesObserver,
) {
suspend fun getAllManga(): List<Manga> {
@@ -40,6 +41,9 @@ class FavouritesRepository @Inject constructor(
}
fun observeAll(order: ListSortOrder, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
if (ListFilterOption.Downloaded in filterOptions) {
return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
}
return db.getFavouritesDao().observeAll(order, filterOptions, limit)
.mapItems { it.toManga() }
}
@@ -55,6 +59,9 @@ class FavouritesRepository @Inject constructor(
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> {
if (ListFilterOption.Downloaded in filterOptions) {
return localObserver.observeAll(categoryId, order, filterOptions - ListFilterOption.Downloaded, limit)
}
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
.mapItems { it.toManga() }
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.favourites.domain
import dagger.Reusable
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.data.FavouriteManga
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.LocalObserveMapper
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@Reusable
class LocalFavoritesObserver @Inject constructor(
localMangaRepository: LocalMangaRepository,
private val db: MangaDatabase,
) : LocalObserveMapper<FavouriteManga, Manga>(localMangaRepository, limitStep = 10) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> = observe(limit) { newLimit ->
db.getFavouritesDao().observeAll(order, filterOptions, newLimit)
}
fun observeAll(
categoryId: Long,
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<Manga>> = observe(limit) { newLimit ->
db.getFavouritesDao().observeAll(categoryId, order, filterOptions, newLimit)
}
override fun toManga(e: FavouriteManga) = e.manga.toManga(e.tags.toMangaTags())
override fun toResult(e: FavouriteManga, manga: Manga) = manga
}

View File

@@ -4,26 +4,22 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
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.core.util.ext.flattenLatest
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
@@ -39,12 +35,11 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val PAGE_SIZE = 20
private const val PAGE_SIZE = 16
@HiltViewModel
class FavouritesListViewModel @Inject constructor(
@@ -53,7 +48,6 @@ class FavouritesListViewModel @Inject constructor(
private val mangaListMapper: MangaListMapper,
private val markAsReadUseCase: MarkAsReadUseCase,
private val quickFilter: FavoritesListQuickFilter,
private val localMangaRepository: LocalMangaRepository,
settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
@@ -130,30 +124,32 @@ class FavouritesListViewModel @Inject constructor(
}
private suspend fun List<Manga>.mapList(mode: ListMode, filters: Set<ListFilterOption>): List<ListModel> {
val list = if (ListFilterOption.Downloaded in filters) {
mapToLocal()
} else {
this
}
if (list.isEmpty()) {
if (isEmpty()) {
return if (filters.isEmpty()) {
listOf(getEmptyState(hasFilters = false))
} else {
listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
}
}
val result = ArrayList<ListModel>(list.size + 1)
val result = ArrayList<ListModel>(size + 1)
quickFilter.filterItem(filters)?.let(result::add)
mangaListMapper.toListModelList(result, list, mode)
mangaListMapper.toListModelList(result, this, mode)
return result
}
private fun observeFavorites() = if (categoryId == NO_ID) {
combine(sortOrder.filterNotNull(), quickFilter.appliedOptions.combineWithSettings(), limit, ::Triple)
.flatMapLatest { repository.observeAll(it.first, it.second - ListFilterOption.Downloaded, it.third) }
combine(
sortOrder.filterNotNull(),
quickFilter.appliedOptions.combineWithSettings(),
limit,
) { order, filters, limit ->
isReady.set(false)
repository.observeAll(order, filters, limit)
}.flattenLatest()
} else {
combine(quickFilter.appliedOptions, limit, ::Pair)
.flatMapLatest { repository.observeAll(categoryId, it.first - ListFilterOption.Downloaded, it.second) }
combine(quickFilter.appliedOptions.combineWithSettings(), limit) { filters, limit ->
repository.observeAll(categoryId, filters, limit)
}.flattenLatest()
}
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
@@ -175,16 +171,4 @@ class FavouritesListViewModel @Inject constructor(
actionStringRes = 0,
)
}
private suspend fun List<Manga>.mapToLocal(): List<Manga> = coroutineScope {
map {
async {
if (it.isLocal) {
it
} else {
localMangaRepository.findSavedManga(it)?.manga
}
}
}.awaitAll().filterNotNull()
}
}

View File

@@ -230,10 +230,21 @@ class FilterCoordinator @Inject constructor(
}
override fun setSortOrder(value: SortOrder) {
currentState.update { oldValue ->
oldValue.copy(sortOrder = value)
val available = repository.sortOrders
val sortOrder = if (value !in available) {
val generic = GenericSortOrder.of(value)
when {
generic.ascending in available -> generic.ascending
generic.descending in available -> generic.descending
else -> return
}
} else {
value
}
repository.defaultSortOrder = value
currentState.update { oldValue ->
oldValue.copy(sortOrder = sortOrder)
}
repository.defaultSortOrder = sortOrder
}
override fun setLanguage(value: Locale?) {

View File

@@ -71,7 +71,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
if (isChecked) {
setSortDirection(getSortDirection(checkedId))
setSortDirection(getSortDirection(checkedId) ?: return)
}
}
@@ -81,7 +81,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
R.id.spinner_order -> {
val genericOrder = filter.filterSortOrder.value.availableItems[position]
val direction = getSortDirection(requireViewBinding().layoutSortDirection.checkedButtonId)
filter.setSortOrder(genericOrder[direction])
filter.setSortOrder(genericOrder[direction ?: SortDirection.DESC])
}
R.id.spinner_locale -> filter.setLanguage(filter.filterLocale.value.availableItems[position])
@@ -280,10 +280,10 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
filter.setSortOrder(newOrder)
}
private fun getSortDirection(@IdRes buttonId: Int): SortDirection = when (buttonId) {
private fun getSortDirection(@IdRes buttonId: Int): SortDirection? = when (buttonId) {
R.id.button_order_asc -> SortDirection.ASC
R.id.button_order_desc -> SortDirection.DESC
else -> throw IllegalArgumentException("Wrong button id $buttonId")
else -> null
}
companion object {

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.history.data
import dagger.Reusable
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.domain.LocalObserveMapper
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@Reusable
class HistoryLocalObserver @Inject constructor(
localMangaRepository: LocalMangaRepository,
private val db: MangaDatabase,
) : LocalObserveMapper<HistoryWithManga, MangaWithHistory>(localMangaRepository, limitStep = 10) {
fun observeAll(
order: ListSortOrder,
filterOptions: Set<ListFilterOption>,
limit: Int
) = observe(limit) { newLimit ->
db.getHistoryDao().observeAll(order, filterOptions, newLimit)
}
override fun toManga(e: HistoryWithManga) = e.manga.toManga(e.tags.toMangaTags())
override fun toResult(e: HistoryWithManga, manga: Manga) = MangaWithHistory(
manga = manga,
history = e.history.toMangaHistory(),
)
}

View File

@@ -39,6 +39,7 @@ class HistoryRepository @Inject constructor(
private val settings: AppSettings,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val mangaRepository: MangaDataRepository,
private val localObserver: HistoryLocalObserver,
private val newChaptersUseCaseProvider: Provider<CheckNewChaptersUseCase>,
) {
@@ -80,6 +81,9 @@ class HistoryRepository @Inject constructor(
filterOptions: Set<ListFilterOption>,
limit: Int
): Flow<List<MangaWithHistory>> {
if (ListFilterOption.Downloaded in filterOptions) {
return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
}
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()),

View File

@@ -3,21 +3,16 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -25,6 +20,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.flattenLatest
import org.koitharu.kotatsu.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
@@ -42,20 +38,18 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val PAGE_SIZE = 20
private const val PAGE_SIZE = 16
@HiltViewModel
class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository,
settings: AppSettings,
private val mangaListMapper: MangaListMapper,
private val localMangaRepository: LocalMangaRepository,
private val markAsReadUseCase: MarkAsReadUseCase,
private val quickFilter: HistoryListQuickFilter,
downloadScheduler: DownloadWorker.Scheduler,
@@ -144,21 +138,22 @@ class HistoryListViewModel @Inject constructor(
}
}
private fun observeHistory() = combine(sortOrder, quickFilter.appliedOptions.combineWithSettings(), limit, ::Triple)
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) }
private fun observeHistory() = combine(
sortOrder,
quickFilter.appliedOptions.combineWithSettings(),
limit,
) { order, filters, limit ->
isReady.set(false)
repository.observeAllWithHistory(order, filters, limit)
}.flattenLatest()
private suspend fun mapList(
historyList: List<MangaWithHistory>,
list: List<MangaWithHistory>,
grouped: Boolean,
mode: ListMode,
filters: Set<ListFilterOption>,
isIncognito: Boolean,
): List<ListModel> {
val list = if (ListFilterOption.Downloaded in filters) {
historyList.mapToLocal()
} else {
historyList
}
if (list.isEmpty()) {
return if (filters.isEmpty()) {
listOf(getEmptyState(hasFilters = false))
@@ -198,20 +193,6 @@ class HistoryListViewModel @Inject constructor(
return result
}
private suspend fun List<MangaWithHistory>.mapToLocal() = coroutineScope {
map {
async {
if (it.manga.isLocal) {
it
} else {
localMangaRepository.findSavedManga(it.manga)?.let { localManga ->
MangaWithHistory(localManga.manga, it.history)
}
}
}
}.awaitAll().filterNotNull()
}
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
ListSortOrder.LAST_READ,
ListSortOrder.LONG_AGO_READ -> ListHeader(calculateTimeAgo(updatedAt))

View File

@@ -0,0 +1,55 @@
package org.koitharu.kotatsu.local.domain
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.transformLatest
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
abstract class LocalObserveMapper<E, R>(
private val localMangaRepository: LocalMangaRepository,
private val limitStep: Int,
) {
protected fun observe(limit: Int, observer: (limit: Int) -> Flow<List<E>>): Flow<List<R>> {
val floatingLimit = MutableStateFlow(limit)
return floatingLimit.flatMapLatest { l ->
observer(l)
.transformLatest { fullList ->
val mapped = fullList.mapToLocal()
if (mapped.size < limit && fullList.size == l) {
floatingLimit.value += limitStep
} else {
emit(mapped.take(limit))
}
}.distinctUntilChanged()
}
}
private suspend fun List<E>.mapToLocal(): List<R> = coroutineScope {
val dispatcher = Dispatchers.IO.limitedParallelism(6)
map {
async(dispatcher) {
val m = toManga(it)
val mapped = if (m.isLocal) {
m
} else {
localMangaRepository.findSavedManga(m)?.manga
}
mapped?.let { mm -> toResult(it, mm) }
}
}.awaitAll().filterNotNull()
}
protected abstract fun toManga(e: E): Manga
protected abstract fun toResult(e: E, manga: Manga): R
}

View File

@@ -213,6 +213,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
left = insets.left,
right = insets.right,
)
viewBinding.bottomNav?.updatePadding(bottom = insets.bottom)
}
override fun onLayoutChange(

View File

@@ -6,6 +6,7 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.MultiMutex
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository
@@ -41,26 +42,30 @@ class CheckNewChaptersUseCase @Inject constructor(
}
suspend operator fun invoke(manga: Manga, currentChapterId: Long) = mutex.withLock(manga.id) {
repository.updateTracks()
val details = getFullManga(manga)
val chapters = details.chapters ?: return@withLock
val track = repository.getTrackOrNull(manga) ?: return@withLock
val chapterIndex = chapters.indexOfFirst { x -> x.id == currentChapterId }
val lastNewChapterIndex = chapters.size - track.newChapters
val lastChapter = chapters.lastOrNull()
val tracking = MangaTracking(
manga = details,
lastChapterId = lastChapter?.id ?: 0L,
lastCheck = Instant.now(),
lastChapterDate = lastChapter?.uploadDate?.toInstantOrNull() ?: track.lastChapterDate,
newChapters = when {
track.newChapters == 0 -> 0
chapterIndex < 0 -> track.newChapters
chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex
else -> track.newChapters
},
)
repository.mergeWith(tracking)
runCatchingCancellable {
repository.updateTracks()
val details = getFullManga(manga)
val chapters = details.chapters ?: return@withLock
val track = repository.getTrackOrNull(manga) ?: return@withLock
val chapterIndex = chapters.indexOfFirst { x -> x.id == currentChapterId }
val lastNewChapterIndex = chapters.size - track.newChapters
val lastChapter = chapters.lastOrNull()
val tracking = MangaTracking(
manga = details,
lastChapterId = lastChapter?.id ?: 0L,
lastCheck = Instant.now(),
lastChapterDate = lastChapter?.uploadDate?.toInstantOrNull() ?: track.lastChapterDate,
newChapters = when {
track.newChapters == 0 -> 0
chapterIndex < 0 -> track.newChapters
chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex
else -> track.newChapters
},
)
repository.mergeWith(tracking)
}.onFailure { e ->
e.printStackTraceDebug()
}.isSuccess
}
private suspend fun invokeImpl(track: MangaTracking): MangaUpdates = runCatchingCancellable {

View File

@@ -22,15 +22,15 @@
android:fitsSystemWindows="false"
android:paddingHorizontal="16dp"
android:stateListAnimator="@null"
app:liftOnScrollColor="@null"
app:liftOnScroll="false">
app:liftOnScroll="false"
app:liftOnScrollColor="@null">
<org.koitharu.kotatsu.core.ui.widgets.WindowInsetHolder
android:id="@+id/insetsHolder"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<FrameLayout

View File

@@ -682,4 +682,6 @@
<string name="popularity">Popularidad</string>
<string name="scrobbler_auth_required">Iniciar sesión en %s para continuar</string>
<string name="scrobbler_auth_intro">Inicia sesión para configurar la integración con %s . Esto te permitirá seguir tu progreso de lectura del manga</string>
<string name="unstable_feature">Función inestable</string>
<string name="unstable_feature_summary">Esta función es experimental. Por favor, asegúrate de tener una copia de seguridad para evitar la pérdida de datos</string>
</resources>

View File

@@ -682,4 +682,6 @@
<string name="by_date">Petsa</string>
<string name="scrobbler_auth_required">Mag sign in sa %s upang magpatuloy</string>
<string name="scrobbler_auth_intro">Mag sign in para mag set up ng integration ng %s. Ito ay magbibigay-daan sa iyo na ma-track ang iyong progress at status sa pagbabasa ng manga</string>
<string name="unstable_feature">Hindi matatag ang katangian</string>
<string name="unstable_feature_summary">Ang function na ito ay pang-eksperimento. Pakitiyak na mayroon kang backup upang maiwasan ang pagkawala ng data</string>
</resources>

View File

@@ -678,4 +678,10 @@
<string name="by_date">Data</string>
<string name="popularity">Popularność</string>
<string name="updated_long_ago">Zaktualizowano dawno temu</string>
<string name="scrobbler_auth_required">Zaloguj się do %s aby kontynuować</string>
<string name="scrobbler_auth_intro">Zaloguj się, aby skonfigurować integrację z %s. Umożliwi to śledzenie postępów i statusu czytania mangi</string>
<string name="unstable_feature">Niestabilna funkcja</string>
<string name="unstable_feature_summary">Ta funkcja jest eksperymentalna. Upewnij się, że masz kopię zapasową, aby uniknąć utraty danych</string>
<string name="sort_order_asc">Rosnąco</string>
<string name="sort_order_desc">Malejąco</string>
</resources>

View File

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

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="search">Cauta</string>
<string name="newest">Cele mai noi</string>
<string name="light">Deschis</string>
<string name="dark">Inchis</string>
<string name="share_image">Distribuie imaginea</string>
<string name="pages">Pagini</string>
<string name="_import">Importeaza</string>
<string name="clear_pages_cache">Goleste cacheul pagini</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="_continue">Continua</string>
<string name="nothing_found">Nu s-a găsit nimic</string>
<string name="favourites">Favorite</string>
<string name="history">Istoric</string>
<string name="error_occurred">A aparut o eroare</string>
<string name="list_mode">Modul lista</string>
<string name="remote_sources">Surse manga</string>
<string name="loading_">Se încarcă…</string>
<string name="computing_">Se calculează…</string>
<string name="close">Inchide</string>
<string name="retry">Reîncearcă</string>
<string name="read">Citeste</string>
<string name="you_have_not_favourites_yet">Nici un favorit inca</string>
<string name="create_shortcut">Creeaza scurtatura…</string>
<string name="share_s">Distribuie %s</string>
<string name="manga_downloading_">Se descarca…</string>
<string name="processing_">Se proceseaza…</string>
<string name="download_complete">Descarcat</string>
<string name="downloads">Descarcari</string>
<string name="by_name">Nume</string>
<string name="popular">Popular</string>
<string name="updated">Actualizat</string>
<string name="by_rating">Evaluare</string>
<string name="sort_order">Ordine de sortare</string>
<string name="filter">Filtre</string>
<string name="theme">Temă</string>
<string name="follow_system">Urmeaza setarile implicite</string>
<string name="clear">Goleste</string>
<string name="remove">Sterge</string>
<string name="_s_deleted_from_local_storage">\"%s\" sters din stocarea locala</string>
<string name="save_page">Salveaza pagina</string>
<string name="page_saved">Salvat</string>
<string name="delete">Sterge</string>
<string name="operation_not_supported">Această operațiune nu este acceptată</string>
<string name="text_file_not_supported">Alege fie un fișier ZIP, fie un fișier CBZ.</string>
<string name="no_description">Fara descriptie</string>
<string name="standard">Standard</string>
<string name="webtoon">Webtoon</string>
<string name="read_mode">Mod citire</string>
<string name="grid_size">Marime grila</string>
<string name="search_on_s">Cauta pe %s</string>
<string name="delete_manga">Sterge manga</string>
<string name="text_delete_local_manga">Sterge permanent \"%s\" de pe acest dizpozitiv?</string>
<string name="reader_settings">Setari cititor</string>
<string name="switch_pages">Schimba paginile</string>
<string name="error">Erroare</string>
<string name="clear_thumbs_cache">Șterge memoria cache a thumbnails</string>
<string name="clear_search_history">Sterge istoricul cautarilor</string>
<string name="list">Lista</string>
<string name="detailed_list">Lista detaliata</string>
<string name="chapters">Capitole</string>
<string name="local_storage">Stocare Locala</string>
<string name="details">Detalii</string>
<string name="grid">Grilă</string>
<string name="network_error">Eroare de rețea</string>
<string name="settings">Setari</string>
<string name="chapter_d_of_d">Capitolul %1$d din %2$d</string>
<string name="try_again">Incearca din nou</string>
<string name="clear_history">Sterge istoric</string>
<string name="history_is_empty">Nici un istoric inca</string>
<string name="share">Distribuie</string>
<string name="search_manga">Cauta manga</string>
<string name="add_to_favourites">Adaugă la favorite</string>
<string name="add_new_category">Categorie noua</string>
<string name="add">Adauga</string>
<string name="save">Salveaza</string>
</resources>

View File

@@ -680,4 +680,8 @@
<string name="by_date">Tarih</string>
<string name="sort_order_desc">Azalan</string>
<string name="popularity">Popülerlik</string>
</resources>
<string name="scrobbler_auth_required">Devam etmek için %s\'de oturum açın</string>
<string name="scrobbler_auth_intro">%s ile bütünleşmeyi ayarlamak için oturum açın. Bu, manga okuma ilerlemenizi ve durumunuzu izlemenizi sağlayacaktır</string>
<string name="unstable_feature">Kararsız özellik</string>
<string name="unstable_feature_summary">Bu işlev deneyseldir. Veri kaybını önlemek için lütfen yedeğiniz olduğundan emin olun</string>
</resources>

View File

@@ -680,4 +680,8 @@
<string name="by_date">Theo ngày</string>
<string name="popularity">Theo mức độ phổ biến</string>
<string name="updated_long_ago">Đã được cập nhật từ trước đó</string>
</resources>
<string name="scrobbler_auth_required">Đăng nhập vào %s để tiếp tục</string>
<string name="unstable_feature">Tính năng không ổn định</string>
<string name="scrobbler_auth_intro">Đăng nhập để thiết đặt với %s. Điều này sẽ cho phép bạn theo dõi tiến trình và trạng thái đọc manga của mình</string>
<string name="unstable_feature_summary">Tính năng này đang được thử nghiệm. Hãy chắc chắn rằng bạn đã tạo bản sao lưu để tránh việc mất dữ liệu oan</string>
</resources>

View File

@@ -680,4 +680,8 @@
<string name="sort_order_desc">降序</string>
<string name="by_date">日期</string>
<string name="popularity">人气</string>
</resources>
<string name="scrobbler_auth_required">登录 %s 以继续</string>
<string name="scrobbler_auth_intro">登录以连接 %s这个操作会允许记录你的漫画阅读进度和漫画状态</string>
<string name="unstable_feature">不稳定特色功能</string>
<string name="unstable_feature_summary">本功能为实验性功能,请确保你已经备份以防数据丢失</string>
</resources>