Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f09e28e782 | ||
|
|
b601b07586 | ||
|
|
73cea59691 | ||
|
|
e2993d47b6 | ||
|
|
2cd67e7cf8 | ||
|
|
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 = 668
|
||||
versionName = '7.5.2'
|
||||
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'
|
||||
@@ -144,7 +144,7 @@ dependencies {
|
||||
implementation 'ch.acra:acra-http:5.11.3'
|
||||
implementation 'ch.acra:acra-dialog:5.11.3'
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.3'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:3.0-alpha-8'
|
||||
debugImplementation 'com.github.Koitharu:WorkInspector:5778dd1747'
|
||||
|
||||
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -22,3 +22,7 @@
|
||||
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }
|
||||
-keep class org.jsoup.parser.Tag
|
||||
-keep class org.jsoup.internal.StringUtil
|
||||
|
||||
-keep class org.acra.security.NoKeyStoreFactory { *; }
|
||||
-keep class org.acra.config.DefaultRetryPolicy { *; }
|
||||
-keep class org.acra.attachment.DefaultAttachmentProvider { *; }
|
||||
|
||||
@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.Closeable
|
||||
import org.json.JSONArray
|
||||
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
|
||||
@@ -38,7 +39,7 @@ class BackupZipInput private constructor(val file: File) : Closeable {
|
||||
fun cleanupAsync() {
|
||||
processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
|
||||
runCatching {
|
||||
close()
|
||||
closeQuietly()
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
@@ -46,14 +47,22 @@ class BackupZipInput private constructor(val file: File) : Closeable {
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(file: File): BackupZipInput = try {
|
||||
val res = BackupZipInput(file)
|
||||
if (res.zipFile.getEntry("index") == null) {
|
||||
throw BadBackupFormatException(null)
|
||||
fun from(file: File): BackupZipInput {
|
||||
var res: BackupZipInput? = null
|
||||
return try {
|
||||
res = BackupZipInput(file)
|
||||
if (res.zipFile.getEntry("index") == null) {
|
||||
throw BadBackupFormatException(null)
|
||||
}
|
||||
res
|
||||
} catch (exception: Exception) {
|
||||
res?.closeQuietly()
|
||||
throw if (exception is ZipException) {
|
||||
BadBackupFormatException(exception)
|
||||
} else {
|
||||
exception
|
||||
}
|
||||
}
|
||||
res
|
||||
} catch (e: ZipException) {
|
||||
throw BadBackupFormatException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
|
||||
@@ -13,6 +15,9 @@ abstract class PreferencesDao {
|
||||
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
||||
abstract fun observe(mangaId: Long): Flow<MangaPrefsEntity?>
|
||||
|
||||
@Query("UPDATE preferences SET cf_brightness = 0, cf_contrast = 0, cf_invert = 0, cf_grayscale = 0")
|
||||
abstract suspend fun resetColorFilters()
|
||||
|
||||
@Upsert
|
||||
abstract suspend fun upsert(pref: MangaPrefsEntity)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AndroidRuntimeException
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -19,6 +18,7 @@ import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Provider
|
||||
@@ -40,9 +40,10 @@ interface NetworkModule {
|
||||
@Singleton
|
||||
fun provideCookieJar(
|
||||
@ApplicationContext context: Context
|
||||
): MutableCookieJar = try {
|
||||
): MutableCookieJar = runCatching {
|
||||
AndroidCookieJar()
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
}.getOrElse { e ->
|
||||
e.printStackTraceDebug()
|
||||
// WebView is not available
|
||||
PreferencesCookieJar(context)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetColorFilters() {
|
||||
db.getPreferencesDao().resetColorFilters()
|
||||
}
|
||||
|
||||
suspend fun getReaderMode(mangaId: Long): ReaderMode? {
|
||||
return db.getPreferencesDao().find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_READER_AUTOSCROLL_SPEED = "as_speed"
|
||||
const val KEY_MIRROR_SWITCHING = "mirror_switching"
|
||||
const val KEY_PROXY = "proxy"
|
||||
const val KEY_PROXY_TYPE = "proxy_type"
|
||||
const val KEY_PROXY_TYPE = "proxy_type_2"
|
||||
const val KEY_PROXY_ADDRESS = "proxy_address"
|
||||
const val KEY_PROXY_PORT = "proxy_port"
|
||||
const val KEY_PROXY_AUTH = "proxy_auth"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,10 +10,13 @@ import android.provider.OpenableColumns
|
||||
import androidx.core.database.getStringOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.fs.FileSequence
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.InputStream
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
@@ -32,10 +35,19 @@ fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() }
|
||||
|
||||
fun File.isNotEmpty() = length() != 0L
|
||||
|
||||
@Blocking
|
||||
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
|
||||
@Blocking
|
||||
fun ZipFile.getInputStreamOrClose(entry: ZipEntry): InputStream = try {
|
||||
getInputStream(entry)
|
||||
} catch (e: Throwable) {
|
||||
closeQuietly()
|
||||
throw e
|
||||
}
|
||||
|
||||
fun File.getStorageName(context: Context): String = runCatching {
|
||||
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -41,7 +41,7 @@ fun Uri.source(): Source = when (scheme) {
|
||||
URI_SCHEME_ZIP -> {
|
||||
val zip = ZipFile(schemeSpecificPart)
|
||||
val entry = zip.getEntry(fragment)
|
||||
zip.getInputStream(entry).source().withExtraCloseable(zip)
|
||||
zip.getInputStreamOrClose(entry).source().withExtraCloseable(zip)
|
||||
}
|
||||
|
||||
else -> unsupportedUri(this)
|
||||
|
||||
@@ -22,6 +22,7 @@ import okio.source
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.getInputStreamOrClose
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.data.isFileUri
|
||||
import org.koitharu.kotatsu.local.data.isZipUri
|
||||
@@ -68,7 +69,7 @@ class MangaPageFetcher(
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
SourceResult(
|
||||
source = ImageSource(
|
||||
source = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer(),
|
||||
source = zip.getInputStreamOrClose(entry).source().withExtraCloseable(zip).buffer(),
|
||||
context = context,
|
||||
metadata = MangaPageMetadata(page),
|
||||
),
|
||||
|
||||
@@ -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,24 @@ 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.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.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 +37,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 +50,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 {
|
||||
@@ -61,7 +57,7 @@ class FavouritesListViewModel @Inject constructor(
|
||||
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
|
||||
private val refreshTrigger = MutableStateFlow(Any())
|
||||
private val limit = MutableStateFlow(PAGE_SIZE)
|
||||
private val isReady = AtomicBoolean(false)
|
||||
private val isPaginationReady = AtomicBoolean(false)
|
||||
|
||||
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_FAVORITES) { favoritesListMode }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.favoritesListMode)
|
||||
@@ -82,7 +78,9 @@ class FavouritesListViewModel @Inject constructor(
|
||||
observeListModeWithTriggers(),
|
||||
refreshTrigger,
|
||||
) { list, filters, mode, _ ->
|
||||
list.mapList(mode, filters).also { isReady.set(true) }
|
||||
list.mapList(mode, filters)
|
||||
}.distinctUntilChanged().onEach {
|
||||
isPaginationReady.set(true)
|
||||
}.catch {
|
||||
emit(listOf(it.toErrorState(canRetry = false)))
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
@@ -124,36 +122,38 @@ class FavouritesListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun requestMoreItems() {
|
||||
if (isReady.compareAndSet(true, false)) {
|
||||
if (isPaginationReady.compareAndSet(true, false)) {
|
||||
limit.value += PAGE_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
isPaginationReady.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 +175,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])
|
||||
@@ -275,15 +275,15 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
|
||||
private fun setSortDirection(direction: SortDirection) {
|
||||
val filter = requireFilter()
|
||||
val currentOrder = filter.filterSortOrder.value.selectedItems.single()
|
||||
val currentOrder = filter.filterSortOrder.value.selectedItems.singleOrNull() ?: return
|
||||
val newOrder = currentOrder[direction]
|
||||
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,17 @@ 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.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.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,7 +21,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.onFirst
|
||||
import org.koitharu.kotatsu.core.util.ext.flattenLatest
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryListQuickFilter
|
||||
@@ -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,
|
||||
@@ -81,7 +75,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private val limit = MutableStateFlow(PAGE_SIZE)
|
||||
private val isReady = AtomicBoolean(false)
|
||||
private val isPaginationReady = AtomicBoolean(false)
|
||||
|
||||
val isStatsEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
@@ -96,11 +90,9 @@ class HistoryListViewModel @Inject constructor(
|
||||
observeListModeWithTriggers(),
|
||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
||||
) { filters, list, grouped, mode, incognito ->
|
||||
mapList(list, grouped, mode, filters, incognito).also { isReady.set(true) }
|
||||
}.onStart {
|
||||
loadingCounter.increment()
|
||||
}.onFirst {
|
||||
loadingCounter.decrement()
|
||||
mapList(list, grouped, mode, filters, incognito)
|
||||
}.distinctUntilChanged().onEach {
|
||||
isPaginationReady.set(true)
|
||||
}.catch { e ->
|
||||
emit(listOf(e.toErrorState(canRetry = false)))
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
@@ -139,26 +131,27 @@ class HistoryListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun requestMoreItems() {
|
||||
if (isReady.compareAndSet(true, false)) {
|
||||
if (isPaginationReady.compareAndSet(true, false)) {
|
||||
limit.value += PAGE_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
isPaginationReady.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 +191,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))
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.util.ext.getInputStreamOrClose
|
||||
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@@ -24,7 +25,7 @@ class CbzFetcher(
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
|
||||
val bufferedSource = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer()
|
||||
val bufferedSource = zip.getInputStreamOrClose(entry).source().withExtraCloseable(zip).buffer()
|
||||
SourceResult(
|
||||
source = ImageSource(
|
||||
source = bufferedSource,
|
||||
|
||||
@@ -112,32 +112,33 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
||||
return runInterruptible(Dispatchers.IO) {
|
||||
val uri = Uri.parse(chapter.url)
|
||||
val file = uri.toFile()
|
||||
val zip = ZipFile(file)
|
||||
val index = zip.getEntry(LocalMangaOutput.ENTRY_NAME_INDEX)?.let(zip::readText)?.let(::MangaIndex)
|
||||
var entries = zip.entries().asSequence()
|
||||
entries = if (index != null) {
|
||||
val pattern = index.getChapterNamesPattern(chapter)
|
||||
entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
|
||||
} else {
|
||||
val parent = uri.fragment.orEmpty()
|
||||
entries.filter { x ->
|
||||
!x.isDirectory && x.name.substringBeforeLast(
|
||||
File.separatorChar,
|
||||
"",
|
||||
) == parent
|
||||
ZipFile(file).use { zip ->
|
||||
val index = zip.getEntry(LocalMangaOutput.ENTRY_NAME_INDEX)?.let(zip::readText)?.let(::MangaIndex)
|
||||
var entries = zip.entries().asSequence()
|
||||
entries = if (index != null) {
|
||||
val pattern = index.getChapterNamesPattern(chapter)
|
||||
entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
|
||||
} else {
|
||||
val parent = uri.fragment.orEmpty()
|
||||
entries.filter { x ->
|
||||
!x.isDirectory && x.name.substringBeforeLast(
|
||||
File.separatorChar,
|
||||
"",
|
||||
) == parent
|
||||
}
|
||||
}
|
||||
entries
|
||||
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
|
||||
.map { x ->
|
||||
val entryUri = zipUri(file, x.name)
|
||||
MangaPage(
|
||||
id = entryUri.longHashCode(),
|
||||
url = entryUri,
|
||||
preview = null,
|
||||
source = LocalMangaSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
entries
|
||||
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
|
||||
.map { x ->
|
||||
val entryUri = zipUri(file, x.name)
|
||||
MangaPage(
|
||||
id = entryUri.longHashCode(),
|
||||
url = entryUri,
|
||||
preview = null,
|
||||
source = LocalMangaSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.koitharu.kotatsu.local.domain
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
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
|
||||
import java.util.Collections
|
||||
import java.util.WeakHashMap
|
||||
|
||||
abstract class LocalObserveMapper<E : Any, R : Any>(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val limitStep: Int,
|
||||
) {
|
||||
|
||||
private val cache = Collections.synchronizedMap(WeakHashMap<Manga, R?>())
|
||||
|
||||
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(cache)
|
||||
if (mapped.size < limit && fullList.size == l) {
|
||||
floatingLimit.value += limitStep
|
||||
} else {
|
||||
emit(mapped.take(limit))
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun List<E>.mapToLocal(cache: MutableMap<Manga, R?>): List<R> = coroutineScope {
|
||||
val dispatcher = Dispatchers.IO.limitedParallelism(8)
|
||||
map { item ->
|
||||
val m = toManga(item)
|
||||
if (cache.contains(m)) {
|
||||
CompletableDeferred(cache[m])
|
||||
} else async(dispatcher) {
|
||||
val mapped = if (m.isLocal) {
|
||||
m
|
||||
} else {
|
||||
localMangaRepository.findSavedManga(m)?.manga
|
||||
}
|
||||
mapped?.let { mm -> toResult(item, mm) }.also { cache[m] = it }
|
||||
}
|
||||
}.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(
|
||||
|
||||
@@ -67,10 +67,11 @@ class DetectReaderModeUseCase @Inject constructor(
|
||||
|
||||
val size = when {
|
||||
uri.isZipUri() -> runInterruptible(Dispatchers.IO) {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
getBitmapSize(it)
|
||||
ZipFile(uri.schemeSpecificPart).use { zip ->
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
getBitmapSize(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,7 @@ class ColorFilterConfigViewModel @Inject constructor(
|
||||
fun saveGlobally() {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
settings.readerColorFilter = colorFilter.value
|
||||
if (mangaDataRepository.getColorFilter(manga.id) != null) {
|
||||
mangaDataRepository.saveColorFilter(manga, colorFilter.value)
|
||||
}
|
||||
mangaDataRepository.resetColorFilters()
|
||||
onDismiss.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -7,7 +7,7 @@
|
||||
android:defaultValue="DIRECT"
|
||||
android:entries="@array/proxy_types"
|
||||
android:entryValues="@array/values_proxy_types"
|
||||
android:key="proxy_type"
|
||||
android:key="proxy_type_2"
|
||||
android:title="@string/type"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user