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'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 666
|
versionCode = 667
|
||||||
versionName = '7.5'
|
versionName = '7.5.1'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||||
ksp {
|
ksp {
|
||||||
@@ -83,23 +83,23 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:b404b44008') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:ad726a3fd7') {
|
||||||
exclude group: 'org.json', module: 'json'
|
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.kotlin:kotlin-stdlib:2.0.10'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC.2'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
implementation 'androidx.core:core-ktx:1.13.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.9.1'
|
implementation 'androidx.activity:activity-ktx:1.9.2'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.8.2'
|
implementation 'androidx.fragment:fragment-ktx:1.8.3'
|
||||||
implementation 'androidx.transition:transition-ktx:1.5.1'
|
implementation 'androidx.transition:transition-ktx:1.5.1'
|
||||||
implementation 'androidx.collection:collection-ktx:1.4.3'
|
implementation 'androidx.collection:collection-ktx:1.4.3'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5'
|
||||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-service:2.8.5'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
|
implementation 'androidx.lifecycle:lifecycle-process:2.8.5'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
@@ -107,7 +107,7 @@ dependencies {
|
|||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
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.webkit:webkit:1.11.0'
|
||||||
|
|
||||||
implementation 'androidx.work:work-runtime:2.9.1'
|
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.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -100,11 +99,6 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
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
|
val fm = supportFragmentManager
|
||||||
if (fm.isStateSaved) {
|
if (fm.isStateSaved) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
@@ -101,7 +103,8 @@ fun <T> Flow<T>.withTicker(interval: Long, timeUnit: TimeUnit) = channelFlow<T>
|
|||||||
onCompletion { cause ->
|
onCompletion { cause ->
|
||||||
close(cause)
|
close(cause)
|
||||||
}.combine(tickerFlow(interval, timeUnit)) { x, _ -> x }
|
}.combine(tickerFlow(interval, timeUnit)) { x, _ -> x }
|
||||||
.collectLatest { send(it) }
|
.transformWhile<T, Unit> { trySend(it).isSuccess }
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@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?>.firstNotNull(): T = checkNotNull(first { x -> x != null })
|
||||||
|
|
||||||
suspend fun <T : Any> Flow<T?>.firstNotNullOrNull(): T? = firstOrNull { 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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
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.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -168,7 +169,7 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
dao.observeEnabled(order).map {
|
dao.observeEnabled(order).map {
|
||||||
it.toSources(skipNsfw, order)
|
it.toSources(skipNsfw, order)
|
||||||
}
|
}
|
||||||
}.flatMapLatest { it }
|
}.flattenLatest()
|
||||||
.onStart { assimilateNewSources() }
|
.onStart { assimilateNewSources() }
|
||||||
.combine(observeExternalSources()) { enabled, external ->
|
.combine(observeExternalSources()) { enabled, external ->
|
||||||
val list = ArrayList<MangaSourceInfo>(enabled.size + external.size)
|
val list = ArrayList<MangaSourceInfo>(enabled.size + external.size)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import javax.inject.Inject
|
|||||||
@Reusable
|
@Reusable
|
||||||
class FavouritesRepository @Inject constructor(
|
class FavouritesRepository @Inject constructor(
|
||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
|
private val localObserver: LocalFavoritesObserver,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun getAllManga(): List<Manga> {
|
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>> {
|
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)
|
return db.getFavouritesDao().observeAll(order, filterOptions, limit)
|
||||||
.mapItems { it.toManga() }
|
.mapItems { it.toManga() }
|
||||||
}
|
}
|
||||||
@@ -55,6 +59,9 @@ class FavouritesRepository @Inject constructor(
|
|||||||
filterOptions: Set<ListFilterOption>,
|
filterOptions: Set<ListFilterOption>,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Flow<List<Manga>> {
|
): 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)
|
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
|
||||||
.mapItems { it.toManga() }
|
.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 androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
|
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
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.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
private const val PAGE_SIZE = 16
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class FavouritesListViewModel @Inject constructor(
|
class FavouritesListViewModel @Inject constructor(
|
||||||
@@ -53,7 +48,6 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
private val mangaListMapper: MangaListMapper,
|
private val mangaListMapper: MangaListMapper,
|
||||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||||
private val quickFilter: FavoritesListQuickFilter,
|
private val quickFilter: FavoritesListQuickFilter,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
) : 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> {
|
private suspend fun List<Manga>.mapList(mode: ListMode, filters: Set<ListFilterOption>): List<ListModel> {
|
||||||
val list = if (ListFilterOption.Downloaded in filters) {
|
if (isEmpty()) {
|
||||||
mapToLocal()
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
if (list.isEmpty()) {
|
|
||||||
return if (filters.isEmpty()) {
|
return if (filters.isEmpty()) {
|
||||||
listOf(getEmptyState(hasFilters = false))
|
listOf(getEmptyState(hasFilters = false))
|
||||||
} else {
|
} else {
|
||||||
listOfNotNull(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
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)
|
quickFilter.filterItem(filters)?.let(result::add)
|
||||||
mangaListMapper.toListModelList(result, list, mode)
|
mangaListMapper.toListModelList(result, this, mode)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeFavorites() = if (categoryId == NO_ID) {
|
private fun observeFavorites() = if (categoryId == NO_ID) {
|
||||||
combine(sortOrder.filterNotNull(), quickFilter.appliedOptions.combineWithSettings(), limit, ::Triple)
|
combine(
|
||||||
.flatMapLatest { repository.observeAll(it.first, it.second - ListFilterOption.Downloaded, it.third) }
|
sortOrder.filterNotNull(),
|
||||||
|
quickFilter.appliedOptions.combineWithSettings(),
|
||||||
|
limit,
|
||||||
|
) { order, filters, limit ->
|
||||||
|
isReady.set(false)
|
||||||
|
repository.observeAll(order, filters, limit)
|
||||||
|
}.flattenLatest()
|
||||||
} else {
|
} else {
|
||||||
combine(quickFilter.appliedOptions, limit, ::Pair)
|
combine(quickFilter.appliedOptions.combineWithSettings(), limit) { filters, limit ->
|
||||||
.flatMapLatest { repository.observeAll(categoryId, it.first - ListFilterOption.Downloaded, it.second) }
|
repository.observeAll(categoryId, filters, limit)
|
||||||
|
}.flattenLatest()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
|
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
|
||||||
@@ -175,16 +171,4 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
actionStringRes = 0,
|
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) {
|
override fun setSortOrder(value: SortOrder) {
|
||||||
currentState.update { oldValue ->
|
val available = repository.sortOrders
|
||||||
oldValue.copy(sortOrder = value)
|
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?) {
|
override fun setLanguage(value: Locale?) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
|
|
||||||
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
|
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
setSortDirection(getSortDirection(checkedId))
|
setSortDirection(getSortDirection(checkedId) ?: return)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
R.id.spinner_order -> {
|
R.id.spinner_order -> {
|
||||||
val genericOrder = filter.filterSortOrder.value.availableItems[position]
|
val genericOrder = filter.filterSortOrder.value.availableItems[position]
|
||||||
val direction = getSortDirection(requireViewBinding().layoutSortDirection.checkedButtonId)
|
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])
|
R.id.spinner_locale -> filter.setLanguage(filter.filterLocale.value.availableItems[position])
|
||||||
@@ -280,10 +280,10 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
|||||||
filter.setSortOrder(newOrder)
|
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_asc -> SortDirection.ASC
|
||||||
R.id.button_order_desc -> SortDirection.DESC
|
R.id.button_order_desc -> SortDirection.DESC
|
||||||
else -> throw IllegalArgumentException("Wrong button id $buttonId")
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 settings: AppSettings,
|
||||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||||
private val mangaRepository: MangaDataRepository,
|
private val mangaRepository: MangaDataRepository,
|
||||||
|
private val localObserver: HistoryLocalObserver,
|
||||||
private val newChaptersUseCaseProvider: Provider<CheckNewChaptersUseCase>,
|
private val newChaptersUseCaseProvider: Provider<CheckNewChaptersUseCase>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -80,6 +81,9 @@ class HistoryRepository @Inject constructor(
|
|||||||
filterOptions: Set<ListFilterOption>,
|
filterOptions: Set<ListFilterOption>,
|
||||||
limit: Int
|
limit: Int
|
||||||
): Flow<List<MangaWithHistory>> {
|
): Flow<List<MangaWithHistory>> {
|
||||||
|
if (ListFilterOption.Downloaded in filterOptions) {
|
||||||
|
return localObserver.observeAll(order, filterOptions - ListFilterOption.Downloaded, limit)
|
||||||
|
}
|
||||||
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
|
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
|
||||||
MangaWithHistory(
|
MangaWithHistory(
|
||||||
it.manga.toManga(it.tags.toMangaTags()),
|
it.manga.toManga(it.tags.toMangaTags()),
|
||||||
|
|||||||
@@ -3,21 +3,16 @@ package org.koitharu.kotatsu.history.ui
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
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.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
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.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
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.core.util.ext.onFirst
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
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.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val PAGE_SIZE = 20
|
private const val PAGE_SIZE = 16
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HistoryListViewModel @Inject constructor(
|
class HistoryListViewModel @Inject constructor(
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
private val mangaListMapper: MangaListMapper,
|
private val mangaListMapper: MangaListMapper,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
|
||||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||||
private val quickFilter: HistoryListQuickFilter,
|
private val quickFilter: HistoryListQuickFilter,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
@@ -144,21 +138,22 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeHistory() = combine(sortOrder, quickFilter.appliedOptions.combineWithSettings(), limit, ::Triple)
|
private fun observeHistory() = combine(
|
||||||
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) }
|
sortOrder,
|
||||||
|
quickFilter.appliedOptions.combineWithSettings(),
|
||||||
|
limit,
|
||||||
|
) { order, filters, limit ->
|
||||||
|
isReady.set(false)
|
||||||
|
repository.observeAllWithHistory(order, filters, limit)
|
||||||
|
}.flattenLatest()
|
||||||
|
|
||||||
private suspend fun mapList(
|
private suspend fun mapList(
|
||||||
historyList: List<MangaWithHistory>,
|
list: List<MangaWithHistory>,
|
||||||
grouped: Boolean,
|
grouped: Boolean,
|
||||||
mode: ListMode,
|
mode: ListMode,
|
||||||
filters: Set<ListFilterOption>,
|
filters: Set<ListFilterOption>,
|
||||||
isIncognito: Boolean,
|
isIncognito: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
val list = if (ListFilterOption.Downloaded in filters) {
|
|
||||||
historyList.mapToLocal()
|
|
||||||
} else {
|
|
||||||
historyList
|
|
||||||
}
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return if (filters.isEmpty()) {
|
return if (filters.isEmpty()) {
|
||||||
listOf(getEmptyState(hasFilters = false))
|
listOf(getEmptyState(hasFilters = false))
|
||||||
@@ -198,20 +193,6 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
return result
|
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) {
|
private fun MangaHistory.header(order: ListSortOrder): ListHeader? = when (order) {
|
||||||
ListSortOrder.LAST_READ,
|
ListSortOrder.LAST_READ,
|
||||||
ListSortOrder.LONG_AGO_READ -> ListHeader(calculateTimeAgo(updatedAt))
|
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,
|
left = insets.left,
|
||||||
right = insets.right,
|
right = insets.right,
|
||||||
)
|
)
|
||||||
|
viewBinding.bottomNav?.updatePadding(bottom = insets.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLayoutChange(
|
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.CachingMangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.util.MultiMutex
|
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.core.util.ext.toInstantOrNull
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
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) {
|
suspend operator fun invoke(manga: Manga, currentChapterId: Long) = mutex.withLock(manga.id) {
|
||||||
repository.updateTracks()
|
runCatchingCancellable {
|
||||||
val details = getFullManga(manga)
|
repository.updateTracks()
|
||||||
val chapters = details.chapters ?: return@withLock
|
val details = getFullManga(manga)
|
||||||
val track = repository.getTrackOrNull(manga) ?: return@withLock
|
val chapters = details.chapters ?: return@withLock
|
||||||
val chapterIndex = chapters.indexOfFirst { x -> x.id == currentChapterId }
|
val track = repository.getTrackOrNull(manga) ?: return@withLock
|
||||||
val lastNewChapterIndex = chapters.size - track.newChapters
|
val chapterIndex = chapters.indexOfFirst { x -> x.id == currentChapterId }
|
||||||
val lastChapter = chapters.lastOrNull()
|
val lastNewChapterIndex = chapters.size - track.newChapters
|
||||||
val tracking = MangaTracking(
|
val lastChapter = chapters.lastOrNull()
|
||||||
manga = details,
|
val tracking = MangaTracking(
|
||||||
lastChapterId = lastChapter?.id ?: 0L,
|
manga = details,
|
||||||
lastCheck = Instant.now(),
|
lastChapterId = lastChapter?.id ?: 0L,
|
||||||
lastChapterDate = lastChapter?.uploadDate?.toInstantOrNull() ?: track.lastChapterDate,
|
lastCheck = Instant.now(),
|
||||||
newChapters = when {
|
lastChapterDate = lastChapter?.uploadDate?.toInstantOrNull() ?: track.lastChapterDate,
|
||||||
track.newChapters == 0 -> 0
|
newChapters = when {
|
||||||
chapterIndex < 0 -> track.newChapters
|
track.newChapters == 0 -> 0
|
||||||
chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex
|
chapterIndex < 0 -> track.newChapters
|
||||||
else -> track.newChapters
|
chapterIndex >= lastNewChapterIndex -> chapters.lastIndex - chapterIndex
|
||||||
},
|
else -> track.newChapters
|
||||||
)
|
},
|
||||||
repository.mergeWith(tracking)
|
)
|
||||||
|
repository.mergeWith(tracking)
|
||||||
|
}.onFailure { e ->
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
}.isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun invokeImpl(track: MangaTracking): MangaUpdates = runCatchingCancellable {
|
private suspend fun invokeImpl(track: MangaTracking): MangaUpdates = runCatchingCancellable {
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
android:fitsSystemWindows="false"
|
android:fitsSystemWindows="false"
|
||||||
android:paddingHorizontal="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
android:stateListAnimator="@null"
|
android:stateListAnimator="@null"
|
||||||
app:liftOnScrollColor="@null"
|
app:liftOnScroll="false"
|
||||||
app:liftOnScroll="false">
|
app:liftOnScrollColor="@null">
|
||||||
|
|
||||||
<org.koitharu.kotatsu.core.ui.widgets.WindowInsetHolder
|
<org.koitharu.kotatsu.core.ui.widgets.WindowInsetHolder
|
||||||
android:id="@+id/insetsHolder"
|
android:id="@+id/insetsHolder"
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
app:layout_scrollFlags="scroll|enterAlways|snap" />
|
app:layout_scrollFlags="scroll|enterAlways|snap" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
|||||||
@@ -682,4 +682,6 @@
|
|||||||
<string name="popularity">Popularidad</string>
|
<string name="popularity">Popularidad</string>
|
||||||
<string name="scrobbler_auth_required">Iniciar sesión en %s para continuar</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="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>
|
</resources>
|
||||||
@@ -682,4 +682,6 @@
|
|||||||
<string name="by_date">Petsa</string>
|
<string name="by_date">Petsa</string>
|
||||||
<string name="scrobbler_auth_required">Mag sign in sa %s upang magpatuloy</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="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>
|
</resources>
|
||||||
@@ -678,4 +678,10 @@
|
|||||||
<string name="by_date">Data</string>
|
<string name="by_date">Data</string>
|
||||||
<string name="popularity">Popularność</string>
|
<string name="popularity">Popularność</string>
|
||||||
<string name="updated_long_ago">Zaktualizowano dawno temu</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>
|
</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="by_date">Tarih</string>
|
||||||
<string name="sort_order_desc">Azalan</string>
|
<string name="sort_order_desc">Azalan</string>
|
||||||
<string name="popularity">Popülerlik</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="by_date">Theo ngày</string>
|
||||||
<string name="popularity">Theo mức độ phổ biến</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>
|
<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="sort_order_desc">降序</string>
|
||||||
<string name="by_date">日期</string>
|
<string name="by_date">日期</string>
|
||||||
<string name="popularity">人气</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