Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c51da5a9d5 | ||
|
|
bcfce29610 | ||
|
|
a87d18fae3 | ||
|
|
bbd421445c | ||
|
|
f4e3d797dc | ||
|
|
bd65cbb8b8 | ||
|
|
7d1f81607a | ||
|
|
3b6cd0ea7f | ||
|
|
aff70d8519 | ||
|
|
8a74faa4f0 | ||
|
|
c1ac207809 | ||
|
|
e34e745c84 | ||
|
|
50dd119ab5 | ||
|
|
d0ef177d56 |
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
@@ -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()),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
2
app/src/main/res/values-ro/plurals.xml
Normal file
2
app/src/main/res/values-ro/plurals.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
78
app/src/main/res/values-ro/strings.xml
Normal file
78
app/src/main/res/values-ro/strings.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user