mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Fix formatting in a bunch of files
This commit is contained in:
@@ -23,103 +23,102 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ServerListeners
|
||||
@Inject
|
||||
constructor() {
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
@Inject
|
||||
class ServerListeners {
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private fun <T> Flow<T>.startWith(value: T) = onStart { emit(value) }
|
||||
private fun <T> Flow<T>.startWith(value: T) = onStart { emit(value) }
|
||||
|
||||
private val _mangaListener = MutableSharedFlow<List<Long>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
val mangaListener = _mangaListener.asSharedFlow()
|
||||
private val _mangaListener = MutableSharedFlow<List<Long>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
val mangaListener = _mangaListener.asSharedFlow()
|
||||
|
||||
private val _chapterIdsListener = MutableSharedFlow<List<Long>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
val chapterIdsListener = _chapterIdsListener.asSharedFlow()
|
||||
private val _chapterIdsListener = MutableSharedFlow<List<Long>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
val chapterIdsListener = _chapterIdsListener.asSharedFlow()
|
||||
|
||||
private val _mangaChapterIdsListener = MutableSharedFlow<List<Long>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow()
|
||||
private val _mangaChapterIdsListener = MutableSharedFlow<List<Long>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow()
|
||||
|
||||
private val categoryMangaListener = MutableSharedFlow<Long>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
private val categoryMangaListener = MutableSharedFlow<Long>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
|
||||
private val extensionListener = MutableSharedFlow<List<String>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
private val extensionListener = MutableSharedFlow<List<String>>(
|
||||
extraBufferCapacity = Channel.UNLIMITED,
|
||||
)
|
||||
|
||||
fun <T> combineMangaUpdates(
|
||||
flow: Flow<T>,
|
||||
predate: (suspend (List<Long>) -> Boolean)? = null,
|
||||
) = if (predate != null) {
|
||||
_mangaListener
|
||||
.filter(predate)
|
||||
.startWith(Unit)
|
||||
} else {
|
||||
_mangaListener.startWith(Unit)
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.flatMapLatest { flow }
|
||||
fun <T> combineMangaUpdates(
|
||||
flow: Flow<T>,
|
||||
predate: (suspend (List<Long>) -> Boolean)? = null,
|
||||
) = if (predate != null) {
|
||||
_mangaListener
|
||||
.filter(predate)
|
||||
.startWith(Unit)
|
||||
} else {
|
||||
_mangaListener.startWith(Unit)
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.flatMapLatest { flow }
|
||||
|
||||
fun updateManga(vararg ids: Long) {
|
||||
scope.launch {
|
||||
_mangaListener.emit(ids.toList())
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> combineCategoryManga(
|
||||
flow: Flow<T>,
|
||||
predate: (suspend (Long) -> Boolean)? = null,
|
||||
) = if (predate != null) {
|
||||
categoryMangaListener.filter(predate).startWith(-1)
|
||||
} else {
|
||||
categoryMangaListener.startWith(-1)
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.flatMapLatest { flow }
|
||||
|
||||
fun updateCategoryManga(id: Long) {
|
||||
scope.launch {
|
||||
categoryMangaListener.emit(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> combineChapters(
|
||||
flow: Flow<T>,
|
||||
chapterIdPredate: (suspend (List<Long>) -> Boolean)? = null,
|
||||
mangaIdPredate: (suspend (List<Long>) -> Boolean)? = null,
|
||||
): Flow<T> {
|
||||
val idsListener = _chapterIdsListener
|
||||
.filter { chapterIdPredate?.invoke(it) ?: false }
|
||||
.startWith(Unit)
|
||||
.combine(
|
||||
_mangaChapterIdsListener.filter { mangaIdPredate?.invoke(it) ?: false }
|
||||
.startWith(Unit),
|
||||
) { _, _ -> }
|
||||
|
||||
return idsListener
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.flatMapLatest { flow }
|
||||
}
|
||||
|
||||
fun updateChapters(chapterIds: List<Long>) {
|
||||
scope.launch {
|
||||
_chapterIdsListener.emit(chapterIds)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapters(vararg chapterIds: Long) {
|
||||
scope.launch {
|
||||
_chapterIdsListener.emit(chapterIds.toList())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun updateManga(vararg ids: Long) {
|
||||
scope.launch {
|
||||
_mangaListener.emit(ids.toList())
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> combineCategoryManga(
|
||||
flow: Flow<T>,
|
||||
predate: (suspend (Long) -> Boolean)? = null,
|
||||
) = if (predate != null) {
|
||||
categoryMangaListener.filter(predate).startWith(-1)
|
||||
} else {
|
||||
categoryMangaListener.startWith(-1)
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.flatMapLatest { flow }
|
||||
|
||||
fun updateCategoryManga(id: Long) {
|
||||
scope.launch {
|
||||
categoryMangaListener.emit(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> combineChapters(
|
||||
flow: Flow<T>,
|
||||
chapterIdPredate: (suspend (List<Long>) -> Boolean)? = null,
|
||||
mangaIdPredate: (suspend (List<Long>) -> Boolean)? = null,
|
||||
): Flow<T> {
|
||||
val idsListener = _chapterIdsListener
|
||||
.filter { chapterIdPredate?.invoke(it) ?: false }
|
||||
.startWith(Unit)
|
||||
.combine(
|
||||
_mangaChapterIdsListener.filter { mangaIdPredate?.invoke(it) ?: false }
|
||||
.startWith(Unit),
|
||||
) { _, _ -> }
|
||||
|
||||
return idsListener
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.flatMapLatest { flow }
|
||||
}
|
||||
|
||||
fun updateChapters(chapterIds: List<Long>) {
|
||||
scope.launch {
|
||||
_chapterIdsListener.emit(chapterIds)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapters(vararg chapterIds: Long) {
|
||||
scope.launch {
|
||||
_chapterIdsListener.emit(chapterIds.toList())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,30 +13,29 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ExportBackupFile
|
||||
@Inject
|
||||
constructor(
|
||||
private val backupRepository: BackupRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
includeCategories: Boolean,
|
||||
includeChapters: Boolean,
|
||||
block: HttpRequestBuilder.() -> Unit = {},
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(includeCategories, includeChapters, block)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to export backup" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
includeCategories: Boolean,
|
||||
includeChapters: Boolean,
|
||||
block: HttpRequestBuilder.() -> Unit = {},
|
||||
) = backupRepository.createBackup(includeCategories, includeChapters, block)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class ExportBackupFile(
|
||||
private val backupRepository: BackupRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
includeCategories: Boolean,
|
||||
includeChapters: Boolean,
|
||||
block: HttpRequestBuilder.() -> Unit = {},
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(includeCategories, includeChapters, block)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to export backup" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
includeCategories: Boolean,
|
||||
includeChapters: Boolean,
|
||||
block: HttpRequestBuilder.() -> Unit = {},
|
||||
) = backupRepository.createBackup(includeCategories, includeChapters, block)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,24 +15,23 @@ import okio.Path
|
||||
import okio.SYSTEM
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ImportBackupFile
|
||||
@Inject
|
||||
constructor(
|
||||
private val backupRepository: BackupRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
file: Path,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(file)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to import backup ${file.name}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(file: Path) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file))
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class ImportBackupFile(
|
||||
private val backupRepository: BackupRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
file: Path,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(file)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to import backup ${file.name}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(file: Path) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file))
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,24 +15,23 @@ import okio.Path
|
||||
import okio.SYSTEM
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ValidateBackupFile
|
||||
@Inject
|
||||
constructor(
|
||||
private val backupRepository: BackupRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
file: Path,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(file)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to validate backup ${file.name}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(file: Path) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file))
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class ValidateBackupFile(
|
||||
private val backupRepository: BackupRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
file: Path,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(file)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to validate backup ${file.name}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(file: Path) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file))
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,61 +17,60 @@ import kotlinx.coroutines.flow.map
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class AddMangaToCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId, categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add $mangaId to category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga, category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
) = if (categoryId != 0L) {
|
||||
categoryRepository.addMangaToCategory(mangaId, categoryId)
|
||||
.map { serverListeners.updateCategoryManga(categoryId) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(categoryId)
|
||||
emit(Unit)
|
||||
}
|
||||
@Inject
|
||||
class AddMangaToCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId, categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add $mangaId to category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
) = if (category.id != 0L) {
|
||||
categoryRepository.addMangaToCategory(manga.id, category.id)
|
||||
.map { serverListeners.updateCategoryManga(category.id) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(category.id)
|
||||
emit(Unit)
|
||||
}
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga, category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun asFlow(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
) = if (categoryId != 0L) {
|
||||
categoryRepository.addMangaToCategory(mangaId, categoryId)
|
||||
.map { serverListeners.updateCategoryManga(categoryId) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(categoryId)
|
||||
emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun asFlow(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
) = if (category.id != 0L) {
|
||||
categoryRepository.addMangaToCategory(manga.id, category.id)
|
||||
.map { serverListeners.updateCategoryManga(category.id) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(category.id)
|
||||
emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class CreateCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
name: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(name)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to create category $name" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(name: String) = categoryRepository.createCategory(name)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class CreateCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
name: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(name)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to create category $name" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(name: String) = categoryRepository.createCategory(name)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class DeleteCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete category ${category.name}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(categoryId: Long) = categoryRepository.deleteCategory(categoryId)
|
||||
|
||||
fun asFlow(category: Category) = categoryRepository.deleteCategory(category.id)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class DeleteCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete category ${category.name}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(categoryId: Long) = categoryRepository.deleteCategory(categoryId)
|
||||
|
||||
fun asFlow(category: Category) = categoryRepository.deleteCategory(category.id)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,32 +13,31 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetCategories
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
dropDefault: Boolean = false,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(dropDefault)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(dropDefault: Boolean = false) =
|
||||
categoryRepository.getCategories()
|
||||
.map { categories ->
|
||||
if (dropDefault) {
|
||||
categories.filterNot { it.name.equals("default", true) }
|
||||
} else {
|
||||
categories
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetCategories(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
dropDefault: Boolean = false,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(dropDefault)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(dropDefault: Boolean = false) =
|
||||
categoryRepository.getCategories()
|
||||
.map { categories ->
|
||||
if (dropDefault) {
|
||||
categories.filterNot { it.name.equals("default", true) }
|
||||
} else {
|
||||
categories
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetMangaCategories
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId)
|
||||
|
||||
fun asFlow(manga: Manga) = categoryRepository.getMangaCategories(manga.id)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetMangaCategories(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId)
|
||||
|
||||
fun asFlow(manga: Manga) = categoryRepository.getMangaCategories(manga.id)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,45 +15,44 @@ import kotlinx.coroutines.flow.take
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetMangaListFromCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga list from category $categoryId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga list from category ${category.name}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(categoryId: Long) =
|
||||
serverListeners.combineCategoryManga(
|
||||
categoryRepository.getMangaFromCategory(categoryId),
|
||||
) { categoryId == it }
|
||||
|
||||
fun asFlow(category: Category) =
|
||||
serverListeners.combineCategoryManga(
|
||||
categoryRepository.getMangaFromCategory(category.id),
|
||||
) { category.id == it }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetMangaListFromCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga list from category $categoryId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga list from category ${category.name}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(categoryId: Long) =
|
||||
serverListeners.combineCategoryManga(
|
||||
categoryRepository.getMangaFromCategory(categoryId),
|
||||
) { categoryId == it }
|
||||
|
||||
fun asFlow(category: Category) =
|
||||
serverListeners.combineCategoryManga(
|
||||
categoryRepository.getMangaFromCategory(category.id),
|
||||
) { category.id == it }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,52 +13,51 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ModifyCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
name: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(
|
||||
categoryId = categoryId,
|
||||
name = name,
|
||||
).catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to modify category $categoryId with options: name=$name" }
|
||||
}.collect()
|
||||
@Inject
|
||||
class ModifyCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
name: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(
|
||||
categoryId = categoryId,
|
||||
name = name,
|
||||
).catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to modify category $categoryId with options: name=$name" }
|
||||
}.collect()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
name: String? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(
|
||||
category = category,
|
||||
name = name,
|
||||
).catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" }
|
||||
}.collect()
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
name: String? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(
|
||||
category = category,
|
||||
name = name,
|
||||
).catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" }
|
||||
}.collect()
|
||||
|
||||
fun asFlow(
|
||||
categoryId: Long,
|
||||
name: String,
|
||||
) = categoryRepository.modifyCategory(
|
||||
categoryId = categoryId,
|
||||
name = name,
|
||||
)
|
||||
fun asFlow(
|
||||
categoryId: Long,
|
||||
name: String,
|
||||
) = categoryRepository.modifyCategory(
|
||||
categoryId = categoryId,
|
||||
name = name,
|
||||
)
|
||||
|
||||
fun asFlow(
|
||||
category: Category,
|
||||
name: String? = null,
|
||||
) = categoryRepository.modifyCategory(
|
||||
categoryId = category.id,
|
||||
name = name ?: category.name,
|
||||
)
|
||||
fun asFlow(
|
||||
category: Category,
|
||||
name: String? = null,
|
||||
) = categoryRepository.modifyCategory(
|
||||
categoryId = category.id,
|
||||
name = name ?: category.name,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,61 +17,60 @@ import kotlinx.coroutines.flow.map
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class RemoveMangaFromCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId, categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove $mangaId from category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga, category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
) = if (categoryId != 0L) {
|
||||
categoryRepository.removeMangaFromCategory(mangaId, categoryId)
|
||||
.map { serverListeners.updateCategoryManga(categoryId) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(categoryId)
|
||||
emit(Unit)
|
||||
}
|
||||
@Inject
|
||||
class RemoveMangaFromCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId, categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove $mangaId from category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
) = if (category.id != 0L) {
|
||||
categoryRepository.removeMangaFromCategory(manga.id, category.id)
|
||||
.map { serverListeners.updateCategoryManga(category.id) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(category.id)
|
||||
emit(Unit)
|
||||
}
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga, category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun asFlow(
|
||||
mangaId: Long,
|
||||
categoryId: Long,
|
||||
) = if (categoryId != 0L) {
|
||||
categoryRepository.removeMangaFromCategory(mangaId, categoryId)
|
||||
.map { serverListeners.updateCategoryManga(categoryId) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(categoryId)
|
||||
emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun asFlow(
|
||||
manga: Manga,
|
||||
category: Category,
|
||||
) = if (category.id != 0L) {
|
||||
categoryRepository.removeMangaFromCategory(manga.id, category.id)
|
||||
.map { serverListeners.updateCategoryManga(category.id) }
|
||||
} else {
|
||||
flow {
|
||||
serverListeners.updateCategoryManga(category.id)
|
||||
emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,27 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ReorderCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
position: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId, position)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to move category $categoryId to $position" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
categoryId: Long,
|
||||
position: Int,
|
||||
) = categoryRepository.reorderCategory(categoryId, position)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class ReorderCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
position: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId, position)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to move category $categoryId to $position" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
categoryId: Long,
|
||||
position: Int,
|
||||
) = categoryRepository.reorderCategory(categoryId, position)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,37 +14,36 @@ import kotlinx.coroutines.flow.flow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateCategoryMeta
|
||||
@Inject
|
||||
constructor(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
example: Int = category.meta.example,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category, example)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
category: Category,
|
||||
example: Int = category.meta.example,
|
||||
) = flow {
|
||||
if (example != category.meta.example) {
|
||||
categoryRepository.updateCategoryMeta(
|
||||
category.id,
|
||||
"example",
|
||||
example.toString(),
|
||||
).collect()
|
||||
}
|
||||
emit(Unit)
|
||||
@Inject
|
||||
class UpdateCategoryMeta(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
example: Int = category.meta.example,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category, example)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun asFlow(
|
||||
category: Category,
|
||||
example: Int = category.meta.example,
|
||||
) = flow {
|
||||
if (example != category.meta.example) {
|
||||
categoryRepository.updateCategoryMeta(
|
||||
category.id,
|
||||
"example",
|
||||
example.toString(),
|
||||
).collect()
|
||||
}
|
||||
emit(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,73 +16,72 @@ import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
class DeleteChapterDownload
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
@JvmName("awaitChapter")
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapterIds: List<Long>,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterIds)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for $chapterIds" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
@JvmName("awaitChapters")
|
||||
suspend fun await(
|
||||
chapters: List<Chapter>,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapters)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for ${chapters.joinToString { it.id.toString() }}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterId: Long) =
|
||||
chapterRepository.deleteDownloadedChapter(chapterId)
|
||||
.onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
@JvmName("asFlowChapter")
|
||||
fun asFlow(chapter: Chapter) =
|
||||
chapterRepository.deleteDownloadedChapter(chapter.id)
|
||||
.onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
fun asFlow(chapterIds: List<Long>) =
|
||||
chapterRepository.deleteDownloadedChapters(chapterIds)
|
||||
.onEach { serverListeners.updateChapters(chapterIds) }
|
||||
|
||||
@JvmName("asFlowChapters")
|
||||
fun asFlow(chapter: List<Chapter>) =
|
||||
chapterRepository.deleteDownloadedChapters(chapter.map { it.id })
|
||||
.onEach { serverListeners.updateChapters(chapter.map { it.id }) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class DeleteChapterDownload(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
@JvmName("awaitChapter")
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapterIds: List<Long>,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterIds)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for $chapterIds" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
@JvmName("awaitChapters")
|
||||
suspend fun await(
|
||||
chapters: List<Chapter>,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapters)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to delete chapter download for ${chapters.joinToString { it.id.toString() }}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterId: Long) =
|
||||
chapterRepository.deleteDownloadedChapter(chapterId)
|
||||
.onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
@JvmName("asFlowChapter")
|
||||
fun asFlow(chapter: Chapter) =
|
||||
chapterRepository.deleteDownloadedChapter(chapter.id)
|
||||
.onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
fun asFlow(chapterIds: List<Long>) =
|
||||
chapterRepository.deleteDownloadedChapters(chapterIds)
|
||||
.onEach { serverListeners.updateChapters(chapterIds) }
|
||||
|
||||
@JvmName("asFlowChapters")
|
||||
fun asFlow(chapter: List<Chapter>) =
|
||||
chapterRepository.deleteDownloadedChapters(chapter.map { it.id })
|
||||
.onEach { serverListeners.updateChapters(chapter.map { it.id }) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,47 +15,46 @@ import kotlinx.coroutines.flow.take
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetChapter
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapter $chapterId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapter ${chapter.index} for ${chapter.mangaId}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(chapterId: Long) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapter(chapterId),
|
||||
chapterIdPredate = { ids -> chapterId in ids },
|
||||
)
|
||||
|
||||
fun asFlow(chapter: Chapter) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapter(chapter.id),
|
||||
chapterIdPredate = { ids -> chapter.id in ids },
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetChapter(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapter $chapterId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapter ${chapter.index} for ${chapter.mangaId}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(chapterId: Long) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapter(chapterId),
|
||||
chapterIdPredate = { ids -> chapterId in ids },
|
||||
)
|
||||
|
||||
fun asFlow(chapter: Chapter) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapter(chapter.id),
|
||||
chapterIdPredate = { ids -> chapter.id in ids },
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,40 +13,39 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetChapterPages
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get pages for $chapterId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
url: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
block: HttpRequestBuilder.() -> Unit,
|
||||
) = asFlow(url, block)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get page $url" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(chapterId: Long) = chapterRepository.getPages(chapterId)
|
||||
|
||||
fun asFlow(
|
||||
url: String,
|
||||
block: HttpRequestBuilder.() -> Unit,
|
||||
) = chapterRepository.getPage(url, block)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetChapterPages(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get pages for $chapterId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
url: String,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
block: HttpRequestBuilder.() -> Unit,
|
||||
) = asFlow(url, block)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get page $url" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(chapterId: Long) = chapterRepository.getPages(chapterId)
|
||||
|
||||
fun asFlow(
|
||||
url: String,
|
||||
block: HttpRequestBuilder.() -> Unit,
|
||||
) = chapterRepository.getPage(url, block)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,47 +15,46 @@ import kotlinx.coroutines.flow.take
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetChapters
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapters for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapters for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapters(mangaId),
|
||||
chapterIdPredate = { ids -> false }, // todo
|
||||
)
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapters(manga.id),
|
||||
chapterIdPredate = { ids -> false }, // todo
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetChapters(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapters for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get chapters for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapters(mangaId),
|
||||
chapterIdPredate = { ids -> false }, // todo
|
||||
)
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
serverListeners.combineChapters(
|
||||
chapterRepository.getChapters(manga.id),
|
||||
chapterIdPredate = { ids -> false }, // todo
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class RefreshChapters
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh chapters for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh chapters for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
chapterRepository.fetchChapters(mangaId)
|
||||
.onEach { serverListeners.updateChapters(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
chapterRepository.fetchChapters(manga.id)
|
||||
.onEach { serverListeners.updateChapters(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class RefreshChapters(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh chapters for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh chapters for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
chapterRepository.fetchChapters(mangaId)
|
||||
.onEach { serverListeners.updateChapters(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
chapterRepository.fetchChapters(manga.id)
|
||||
.onEach { serverListeners.updateChapters(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,115 +16,114 @@ import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
class UpdateChapter
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapter $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapterIds: List<Long>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterIds, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapters $chapterIds" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
@JvmName("awaitChapters")
|
||||
suspend fun await(
|
||||
chapters: List<Chapter>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapters, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapters ${chapters.joinToString { it.id.toString() }}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapterId,
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapter.id,
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
fun asFlow(
|
||||
chapterIds: List<Long>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapters(
|
||||
chapterIds = chapterIds,
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapterIds) }
|
||||
|
||||
@JvmName("asFlowChapters")
|
||||
fun asFlow(
|
||||
chapters: List<Chapter>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapters(
|
||||
chapterIds = chapters.map { it.id },
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapters.map { it.id }) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class UpdateChapter(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapter $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapterIds: List<Long>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterIds, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapters $chapterIds" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
@JvmName("awaitChapters")
|
||||
suspend fun await(
|
||||
chapters: List<Chapter>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapters, bookmarked, read, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter bookmark for chapters ${chapters.joinToString { it.id.toString() }}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapterId,
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapter.id,
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
fun asFlow(
|
||||
chapterIds: List<Long>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapters(
|
||||
chapterIds = chapterIds,
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapterIds) }
|
||||
|
||||
@JvmName("asFlowChapters")
|
||||
fun asFlow(
|
||||
chapters: List<Chapter>,
|
||||
bookmarked: Boolean? = null,
|
||||
read: Boolean? = null,
|
||||
lastPageRead: Int? = null,
|
||||
) = chapterRepository.updateChapters(
|
||||
chapterIds = chapters.map { it.id },
|
||||
bookmarked = bookmarked,
|
||||
read = read,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapters.map { it.id }) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,51 +15,50 @@ import kotlinx.coroutines.flow.onEach
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateChapterLastPageRead
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
lastPageRead: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter last page read for chapter $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
lastPageRead: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
lastPageRead: Int,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapterId,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
lastPageRead: Int,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapter.id,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class UpdateChapterLastPageRead(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
lastPageRead: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter last page read for chapter $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
lastPageRead: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, lastPageRead)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
lastPageRead: Int,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapterId,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
lastPageRead: Int,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapter.id,
|
||||
lastPageRead = lastPageRead,
|
||||
).onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,39 +15,38 @@ import kotlinx.coroutines.flow.flow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateChapterMeta
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
pageOffset: Int = chapter.meta.juiPageOffset,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, pageOffset)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
pageOffset: Int = chapter.meta.juiPageOffset,
|
||||
) = flow {
|
||||
if (pageOffset != chapter.meta.juiPageOffset) {
|
||||
chapterRepository.updateChapterMeta(
|
||||
chapter.id,
|
||||
"juiPageOffset",
|
||||
pageOffset.toString(),
|
||||
).collect()
|
||||
serverListeners.updateChapters(chapter.id)
|
||||
}
|
||||
emit(Unit)
|
||||
@Inject
|
||||
class UpdateChapterMeta(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
pageOffset: Int = chapter.meta.juiPageOffset,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, pageOffset)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
pageOffset: Int = chapter.meta.juiPageOffset,
|
||||
) = flow {
|
||||
if (pageOffset != chapter.meta.juiPageOffset) {
|
||||
chapterRepository.updateChapterMeta(
|
||||
chapter.id,
|
||||
"juiPageOffset",
|
||||
pageOffset.toString(),
|
||||
).collect()
|
||||
serverListeners.updateChapters(chapter.id)
|
||||
}
|
||||
emit(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,59 +15,58 @@ import kotlinx.coroutines.flow.onEach
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateChapterRead
|
||||
@Inject
|
||||
constructor(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
read: Boolean,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, read)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter read status for chapter $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
read: Boolean,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, read)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
read: Boolean,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapterId,
|
||||
read = read,
|
||||
).onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
fun asFlow(
|
||||
chapterIds: List<Long>,
|
||||
read: Boolean,
|
||||
) = chapterRepository.updateChapters(
|
||||
chapterIds = chapterIds,
|
||||
read = read,
|
||||
).onEach { serverListeners.updateChapters(chapterIds) }
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
read: Boolean,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapter.id,
|
||||
read = read,
|
||||
).onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class UpdateChapterRead(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
read: Boolean,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, read)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter read status for chapter $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
chapter: Chapter,
|
||||
read: Boolean,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapter, read)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
read: Boolean,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapterId,
|
||||
read = read,
|
||||
).onEach { serverListeners.updateChapters(chapterId) }
|
||||
|
||||
fun asFlow(
|
||||
chapterIds: List<Long>,
|
||||
read: Boolean,
|
||||
) = chapterRepository.updateChapters(
|
||||
chapterIds = chapterIds,
|
||||
read = read,
|
||||
).onEach { serverListeners.updateChapters(chapterIds) }
|
||||
|
||||
fun asFlow(
|
||||
chapter: Chapter,
|
||||
read: Boolean,
|
||||
) = chapterRepository.updateChapter(
|
||||
chapterId = chapter.id,
|
||||
read = read,
|
||||
).onEach { serverListeners.updateChapters(chapter.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,36 +12,35 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class BatchChapterDownload
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterIds: List<Long>,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterIds)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to queue chapters $chapterIds for a download" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
vararg chapterIds: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(*chapterIds)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to queue chapters ${chapterIds.asList()} for a download" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterIds: List<Long>) = downloadRepository.batchDownload(chapterIds)
|
||||
|
||||
fun asFlow(vararg chapterIds: Long) = downloadRepository.batchDownload(chapterIds.asList())
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class BatchChapterDownload(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterIds: List<Long>,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterIds)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to queue chapters $chapterIds for a download" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
vararg chapterIds: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(*chapterIds)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to queue chapters ${chapterIds.asList()} for a download" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterIds: List<Long>) = downloadRepository.batchDownload(chapterIds)
|
||||
|
||||
fun asFlow(vararg chapterIds: Long) = downloadRepository.batchDownload(chapterIds.asList())
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ClearDownloadQueue
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to clear download queue" }
|
||||
}
|
||||
.collect()
|
||||
@Inject
|
||||
class ClearDownloadQueue(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to clear download queue" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow() = downloadRepository.clearDownloadQueue()
|
||||
fun asFlow() = downloadRepository.clearDownloadQueue()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class QueueChapterDownload
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to queue chapter $chapterId for a download" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterId: Long) = downloadRepository.queueChapterDownload(chapterId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class QueueChapterDownload(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to queue chapter $chapterId for a download" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterId: Long) = downloadRepository.queueChapterDownload(chapterId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,27 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ReorderChapterDownload
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
to: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, to)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to reorder chapter download for $chapterId to $to" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
to: Int,
|
||||
) = downloadRepository.reorderChapterDownload(chapterId, to)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class ReorderChapterDownload(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
to: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId, to)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to reorder chapter download for $chapterId to $to" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
chapterId: Long,
|
||||
to: Int,
|
||||
) = downloadRepository.reorderChapterDownload(chapterId, to)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class StartDownloading
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to start downloader" }
|
||||
}
|
||||
.collect()
|
||||
@Inject
|
||||
class StartDownloading(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to start downloader" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow() = downloadRepository.startDownloading()
|
||||
fun asFlow() = downloadRepository.startDownloading()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class StopChapterDownload
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to stop chapter download for $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterId: Long) = downloadRepository.stopChapterDownload(chapterId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class StopChapterDownload(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
chapterId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(chapterId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to stop chapter download for $chapterId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(chapterId: Long) = downloadRepository.stopChapterDownload(chapterId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class StopDownloading
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to stop downloader" }
|
||||
}
|
||||
.collect()
|
||||
@Inject
|
||||
class StopDownloading(
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to stop downloader" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow() = downloadRepository.stopDownloading()
|
||||
fun asFlow() = downloadRepository.stopDownloading()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,42 +16,40 @@ import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class DownloadService
|
||||
@Inject
|
||||
constructor(
|
||||
serverPreferences: ServerPreferences,
|
||||
client: Http,
|
||||
) : WebsocketService(serverPreferences, client) {
|
||||
override val _status: MutableStateFlow<Status>
|
||||
get() = status
|
||||
@Inject
|
||||
class DownloadService(
|
||||
serverPreferences: ServerPreferences,
|
||||
client: Http,
|
||||
) : WebsocketService(serverPreferences, client) {
|
||||
override val _status: MutableStateFlow<Status>
|
||||
get() = status
|
||||
|
||||
override val query: String
|
||||
get() = "/api/v1/downloads"
|
||||
override val query: String
|
||||
get() = "/api/v1/downloads"
|
||||
|
||||
override suspend fun onReceived(frame: Frame.Text) {
|
||||
val status = json.decodeFromString<DownloadStatus>(frame.readText())
|
||||
downloaderStatus.value = status.status
|
||||
downloadQueue.value = status.queue
|
||||
}
|
||||
|
||||
companion object {
|
||||
val status = MutableStateFlow(Status.STARTING)
|
||||
val downloadQueue = MutableStateFlow(emptyList<DownloadChapter>())
|
||||
val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
|
||||
|
||||
fun registerWatch(mangaId: Long) =
|
||||
downloadQueue
|
||||
.map {
|
||||
it.filter { it.mangaId == mangaId }
|
||||
}
|
||||
|
||||
fun registerWatches(mangaIds: Set<Long>) =
|
||||
downloadQueue
|
||||
.map {
|
||||
it.filter { it.mangaId in mangaIds }
|
||||
}
|
||||
}
|
||||
override suspend fun onReceived(frame: Frame.Text) {
|
||||
val status = json.decodeFromString<DownloadStatus>(frame.readText())
|
||||
downloaderStatus.value = status.status
|
||||
downloadQueue.value = status.queue
|
||||
}
|
||||
|
||||
companion object {
|
||||
val status = MutableStateFlow(Status.STARTING)
|
||||
val downloadQueue = MutableStateFlow(emptyList<DownloadChapter>())
|
||||
val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
|
||||
|
||||
fun registerWatch(mangaId: Long) =
|
||||
downloadQueue
|
||||
.map {
|
||||
it.filter { it.mangaId == mangaId }
|
||||
}
|
||||
|
||||
fun registerWatches(mangaIds: Set<Long>) =
|
||||
downloadQueue
|
||||
.map {
|
||||
it.filter { it.mangaId in mangaIds }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetExtensionList
|
||||
@Inject
|
||||
constructor(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get extension list" }
|
||||
}
|
||||
.singleOrNull()
|
||||
@Inject
|
||||
class GetExtensionList(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get extension list" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = extensionRepository.getExtensionList()
|
||||
fun asFlow() = extensionRepository.getExtensionList()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class InstallExtension
|
||||
@Inject
|
||||
constructor(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
extension: Extension,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(extension)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to install extension ${extension.apkName}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class InstallExtension(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
extension: Extension,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(extension)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to install extension ${extension.apkName}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,24 +15,23 @@ import okio.Path
|
||||
import okio.SYSTEM
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class InstallExtensionFile
|
||||
@Inject
|
||||
constructor(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
path: Path,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(path)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to install extension from $path" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(path: Path) = extensionRepository.installExtension(FileSystem.SYSTEM.source(path))
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class InstallExtensionFile(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
path: Path,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(path)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to install extension from $path" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(path: Path) = extensionRepository.installExtension(FileSystem.SYSTEM.source(path))
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UninstallExtension
|
||||
@Inject
|
||||
constructor(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
extension: Extension,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(extension)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to uninstall extension ${extension.apkName}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class UninstallExtension(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
extension: Extension,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(extension)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to uninstall extension ${extension.apkName}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateExtension
|
||||
@Inject
|
||||
constructor(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
extension: Extension,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(extension)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update extension ${extension.apkName}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class UpdateExtension(
|
||||
private val extensionRepository: ExtensionRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
extension: Extension,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(extension)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update extension ${extension.apkName}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetGlobalMeta
|
||||
@Inject
|
||||
constructor(
|
||||
private val globalRepository: GlobalRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get global meta" }
|
||||
}
|
||||
.singleOrNull()
|
||||
@Inject
|
||||
class GetGlobalMeta(
|
||||
private val globalRepository: GlobalRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get global meta" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = globalRepository.getGlobalMeta()
|
||||
fun asFlow() = globalRepository.getGlobalMeta()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,36 +14,35 @@ import kotlinx.coroutines.flow.flow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateGlobalMeta
|
||||
@Inject
|
||||
constructor(
|
||||
private val globalRepository: GlobalRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
globalMeta: GlobalMeta,
|
||||
example: Int = globalMeta.example,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(globalMeta, example)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update global meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
globalMeta: GlobalMeta,
|
||||
example: Int = globalMeta.example,
|
||||
) = flow {
|
||||
if (example != globalMeta.example) {
|
||||
globalRepository.updateGlobalMeta(
|
||||
"example",
|
||||
example.toString(),
|
||||
).collect()
|
||||
}
|
||||
emit(Unit)
|
||||
@Inject
|
||||
class UpdateGlobalMeta(
|
||||
private val globalRepository: GlobalRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
globalMeta: GlobalMeta,
|
||||
example: Int = globalMeta.example,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(globalMeta, example)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update global meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun asFlow(
|
||||
globalMeta: GlobalMeta,
|
||||
example: Int = globalMeta.example,
|
||||
) = flow {
|
||||
if (example != globalMeta.example) {
|
||||
globalRepository.updateGlobalMeta(
|
||||
"example",
|
||||
example.toString(),
|
||||
).collect()
|
||||
}
|
||||
emit(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class AddMangaToLibrary
|
||||
@Inject
|
||||
constructor(
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add $mangaId to library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
libraryRepository.addMangaToLibrary(mangaId)
|
||||
.onEach { serverListeners.updateManga(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
libraryRepository.addMangaToLibrary(manga.id)
|
||||
.onEach { serverListeners.updateManga(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class AddMangaToLibrary(
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add $mangaId to library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
libraryRepository.addMangaToLibrary(mangaId)
|
||||
.onEach { serverListeners.updateManga(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
libraryRepository.addMangaToLibrary(manga.id)
|
||||
.onEach { serverListeners.updateManga(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class RemoveMangaFromLibrary
|
||||
@Inject
|
||||
constructor(
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove $mangaId from library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
libraryRepository.removeMangaFromLibrary(mangaId)
|
||||
.onEach { serverListeners.updateManga(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
libraryRepository.removeMangaFromLibrary(manga.id)
|
||||
.onEach { serverListeners.updateManga(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class RemoveMangaFromLibrary(
|
||||
private val libraryRepository: LibraryRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove $mangaId from library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from library" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
libraryRepository.removeMangaFromLibrary(mangaId)
|
||||
.onEach { serverListeners.updateManga(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
libraryRepository.removeMangaFromLibrary(manga.id)
|
||||
.onEach { serverListeners.updateManga(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,26 +16,25 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class LibraryUpdateService
|
||||
@Inject
|
||||
constructor(
|
||||
serverPreferences: ServerPreferences,
|
||||
client: Http,
|
||||
) : WebsocketService(serverPreferences, client) {
|
||||
override val _status: MutableStateFlow<Status>
|
||||
get() = status
|
||||
@Inject
|
||||
class LibraryUpdateService(
|
||||
serverPreferences: ServerPreferences,
|
||||
client: Http,
|
||||
) : WebsocketService(serverPreferences, client) {
|
||||
override val _status: MutableStateFlow<Status>
|
||||
get() = status
|
||||
|
||||
override val query: String
|
||||
get() = "/api/v1/update"
|
||||
override val query: String
|
||||
get() = "/api/v1/update"
|
||||
|
||||
override suspend fun onReceived(frame: Frame.Text) {
|
||||
updateStatus.value = json.decodeFromString<UpdateStatus>(frame.readText())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
|
||||
val status = MutableStateFlow(Status.STARTING)
|
||||
val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), emptyMap(), false))
|
||||
}
|
||||
override suspend fun onReceived(frame: Frame.Text) {
|
||||
updateStatus.value = json.decodeFromString<UpdateStatus>(frame.readText())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
|
||||
val status = MutableStateFlow(Status.STARTING)
|
||||
val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), emptyMap(), false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,45 +15,44 @@ import kotlinx.coroutines.flow.take
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetManga
|
||||
@Inject
|
||||
constructor(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
serverListeners.combineMangaUpdates(
|
||||
mangaRepository.getManga(mangaId),
|
||||
) { mangaId in it }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
serverListeners.combineMangaUpdates(
|
||||
mangaRepository.getManga(manga.id),
|
||||
) { manga.id in it }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) =
|
||||
serverListeners.combineMangaUpdates(
|
||||
mangaRepository.getManga(mangaId),
|
||||
) { mangaId in it }
|
||||
|
||||
fun asFlow(manga: Manga) =
|
||||
serverListeners.combineMangaUpdates(
|
||||
mangaRepository.getManga(manga.id),
|
||||
) { manga.id in it }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,39 +16,38 @@ import kotlinx.coroutines.flow.take
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class RefreshManga
|
||||
@Inject
|
||||
constructor(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh manga $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = mangaRepository.refreshManga(mangaId).onEach { serverListeners.updateManga(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) = mangaRepository.refreshManga(manga.id).onEach { serverListeners.updateManga(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class RefreshManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
mangaId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh manga $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga)
|
||||
.take(1)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = mangaRepository.refreshManga(mangaId).onEach { serverListeners.updateManga(mangaId) }
|
||||
|
||||
fun asFlow(manga: Manga) = mangaRepository.refreshManga(manga.id).onEach { serverListeners.updateManga(manga.id) }
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,39 +17,38 @@ import kotlinx.coroutines.flow.flow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateMangaMeta
|
||||
@Inject
|
||||
constructor(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
readerMode: String = manga.meta.juiReaderMode,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga, readerMode)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
manga: Manga,
|
||||
readerMode: String = manga.meta.juiReaderMode.decodeURLQueryComponent(),
|
||||
) = flow {
|
||||
if (readerMode.encodeURLQueryComponent() != manga.meta.juiReaderMode) {
|
||||
mangaRepository.updateMangaMeta(
|
||||
manga.id,
|
||||
"juiReaderMode",
|
||||
readerMode,
|
||||
).collect()
|
||||
serverListeners.updateManga(manga.id)
|
||||
}
|
||||
emit(Unit)
|
||||
@Inject
|
||||
class UpdateMangaMeta(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val serverListeners: ServerListeners,
|
||||
) {
|
||||
suspend fun await(
|
||||
manga: Manga,
|
||||
readerMode: String = manga.meta.juiReaderMode,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manga, readerMode)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun asFlow(
|
||||
manga: Manga,
|
||||
readerMode: String = manga.meta.juiReaderMode.decodeURLQueryComponent(),
|
||||
) = flow {
|
||||
if (readerMode.encodeURLQueryComponent() != manga.meta.juiReaderMode) {
|
||||
mangaRepository.updateMangaMeta(
|
||||
manga.id,
|
||||
"juiReaderMode",
|
||||
readerMode,
|
||||
).collect()
|
||||
serverListeners.updateManga(manga.id)
|
||||
}
|
||||
emit(Unit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,20 +11,19 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class RunMigrations
|
||||
@Inject
|
||||
constructor(
|
||||
private val migrationPreferences: MigrationPreferences,
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
) {
|
||||
fun runMigrations() {
|
||||
val code = migrationPreferences.version().get()
|
||||
if (code <= 0) {
|
||||
readerPreferences.modes().get().forEach {
|
||||
readerPreferences.getMode(it).direction().delete()
|
||||
}
|
||||
migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE)
|
||||
return
|
||||
@Inject
|
||||
class RunMigrations(
|
||||
private val migrationPreferences: MigrationPreferences,
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
) {
|
||||
fun runMigrations() {
|
||||
val code = migrationPreferences.version().get()
|
||||
if (code <= 0) {
|
||||
readerPreferences.modes().get().forEach {
|
||||
readerPreferences.getMode(it).direction().delete()
|
||||
}
|
||||
migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class AboutServer
|
||||
@Inject
|
||||
constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get server information" }
|
||||
}
|
||||
.singleOrNull()
|
||||
@Inject
|
||||
class AboutServer(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get server information" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = settingsRepository.aboutServer()
|
||||
fun asFlow() = settingsRepository.aboutServer()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetSettings
|
||||
@Inject
|
||||
constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to check for server updates" }
|
||||
}
|
||||
.singleOrNull()
|
||||
@Inject
|
||||
class GetSettings(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to check for server updates" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = settingsRepository.getSettings()
|
||||
fun asFlow() = settingsRepository.getSettings()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class SetSettings
|
||||
@Inject
|
||||
constructor(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
input: SetSettingsInput,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(input)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to check for server updates" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class SetSettings(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
input: SetSettingsInput,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(input)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to check for server updates" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetFilterList
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get filter list for ${source.displayName}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get filter list for $sourceId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source) = sourceRepository.getFilterList(source.id)
|
||||
|
||||
fun asFlow(sourceId: Long) = sourceRepository.getFilterList(sourceId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetFilterList(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get filter list for ${source.displayName}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get filter list for $sourceId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source) = sourceRepository.getFilterList(source.id)
|
||||
|
||||
fun asFlow(sourceId: Long) = sourceRepository.getFilterList(sourceId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,44 +13,43 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetLatestManga
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get latest manga from $sourceId on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
source: Source,
|
||||
page: Int,
|
||||
) = sourceRepository.getLatestManga(source.id, page)
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
) = sourceRepository.getLatestManga(sourceId, page)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetLatestManga(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get latest manga from $sourceId on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
source: Source,
|
||||
page: Int,
|
||||
) = sourceRepository.getLatestManga(source.id, page)
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
) = sourceRepository.getLatestManga(sourceId, page)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,44 +13,43 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetPopularManga
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get popular manga from $sourceId on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
source: Source,
|
||||
page: Int,
|
||||
) = sourceRepository.getPopularManga(source.id, page)
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
) = sourceRepository.getPopularManga(sourceId, page)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetPopularManga(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get popular manga from $sourceId on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
source: Source,
|
||||
page: Int,
|
||||
) = sourceRepository.getPopularManga(source.id, page)
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
) = sourceRepository.getPopularManga(sourceId, page)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,64 +14,63 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetSearchManga
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
page: Int,
|
||||
searchTerm: String?,
|
||||
filters: List<SourceFilter>?,
|
||||
onError: suspend (Throwable) -> Unit = {
|
||||
},
|
||||
) = asFlow(source.id, page, searchTerm, filters)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
searchTerm: String?,
|
||||
page: Int,
|
||||
filters: List<SourceFilter>?,
|
||||
onError: suspend (Throwable) -> Unit = {
|
||||
},
|
||||
) = asFlow(sourceId, page, searchTerm, filters)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
source: Source,
|
||||
page: Int,
|
||||
searchTerm: String?,
|
||||
filters: List<SourceFilter>?,
|
||||
) = sourceRepository.getSearchResults(
|
||||
source.id,
|
||||
page,
|
||||
searchTerm?.ifBlank { null },
|
||||
filters,
|
||||
)
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
searchTerm: String?,
|
||||
filters: List<SourceFilter>?,
|
||||
) = sourceRepository.getSearchResults(
|
||||
sourceId,
|
||||
page,
|
||||
searchTerm?.ifBlank { null },
|
||||
filters,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetSearchManga(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
page: Int,
|
||||
searchTerm: String?,
|
||||
filters: List<SourceFilter>?,
|
||||
onError: suspend (Throwable) -> Unit = {
|
||||
},
|
||||
) = asFlow(source.id, page, searchTerm, filters)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
searchTerm: String?,
|
||||
page: Int,
|
||||
filters: List<SourceFilter>?,
|
||||
onError: suspend (Throwable) -> Unit = {
|
||||
},
|
||||
) = asFlow(sourceId, page, searchTerm, filters)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(
|
||||
source: Source,
|
||||
page: Int,
|
||||
searchTerm: String?,
|
||||
filters: List<SourceFilter>?,
|
||||
) = sourceRepository.getSearchResults(
|
||||
source.id,
|
||||
page,
|
||||
searchTerm?.ifBlank { null },
|
||||
filters,
|
||||
)
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
page: Int,
|
||||
searchTerm: String?,
|
||||
filters: List<SourceFilter>?,
|
||||
) = sourceRepository.getSearchResults(
|
||||
sourceId,
|
||||
page,
|
||||
searchTerm?.ifBlank { null },
|
||||
filters,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetSourceList
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get source list" }
|
||||
}
|
||||
.singleOrNull()
|
||||
@Inject
|
||||
class GetSourceList(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get source list" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow() = sourceRepository.getSourceList()
|
||||
fun asFlow() = sourceRepository.getSourceList()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetSourceSettings
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get source settings for ${source.displayName}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get source settings for $sourceId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source) = sourceRepository.getSourceSettings(source.id)
|
||||
|
||||
fun asFlow(sourceId: Long) = sourceRepository.getSourceSettings(sourceId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetSourceSettings(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
source: Source,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(source.id)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get source settings for ${source.displayName}" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(sourceId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get source settings for $sourceId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source) = sourceRepository.getSourceSettings(source.id)
|
||||
|
||||
fun asFlow(sourceId: Long) = sourceRepository.getSourceSettings(sourceId)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,32 +13,31 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class SetSourceSetting
|
||||
@Inject
|
||||
constructor(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
sourcePreference: SourcePreference,
|
||||
onError: suspend (Throwable) -> Unit = {
|
||||
},
|
||||
) = asFlow(sourceId, sourcePreference)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to set setting for $sourceId with index = ${sourcePreference.position}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
sourcePreference: SourcePreference,
|
||||
) = sourceRepository.setSourceSetting(
|
||||
sourceId,
|
||||
sourcePreference,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class SetSourceSetting(
|
||||
private val sourceRepository: SourceRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
sourceId: Long,
|
||||
sourcePreference: SourcePreference,
|
||||
onError: suspend (Throwable) -> Unit = {
|
||||
},
|
||||
) = asFlow(sourceId, sourcePreference)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to set setting for $sourceId with index = ${sourcePreference.position}" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(
|
||||
sourceId: Long,
|
||||
sourcePreference: SourcePreference,
|
||||
) = sourceRepository.setSourceSetting(
|
||||
sourceId,
|
||||
sourcePreference,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,21 +33,21 @@ fun interface GetMangaPage {
|
||||
suspend fun get(page: Int): MangaPage?
|
||||
}
|
||||
|
||||
class SourcePager
|
||||
@Inject
|
||||
constructor(
|
||||
private val getManga: GetManga,
|
||||
private val serverListeners: ServerListeners,
|
||||
@Assisted private val fetcher: GetMangaPage,
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||
private val sourceMutex = Mutex()
|
||||
@Inject
|
||||
class SourcePager(
|
||||
private val getManga: GetManga,
|
||||
private val serverListeners: ServerListeners,
|
||||
@Assisted private val fetcher: GetMangaPage,
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||
private val sourceMutex = Mutex()
|
||||
|
||||
private val _sourceManga = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val _sourceManga = MutableStateFlow<List<Manga>>(emptyList())
|
||||
|
||||
private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } }
|
||||
.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } }
|
||||
.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val changedManga = serverListeners.mangaListener.runningFold(emptyMap<Long, Manga>()) { manga, updatedMangaIds ->
|
||||
private val changedManga =
|
||||
serverListeners.mangaListener.runningFold(emptyMap<Long, Manga>()) { manga, updatedMangaIds ->
|
||||
coroutineScope {
|
||||
manga + updatedMangaIds.filter { it in mangaIds.value }.map {
|
||||
async {
|
||||
@@ -57,37 +57,37 @@ class SourcePager
|
||||
}
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga ->
|
||||
sourceManga.map { changedManga[it.id] ?: it }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga ->
|
||||
sourceManga.map { changedManga[it.id] ?: it }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val _pageNum = MutableStateFlow(0)
|
||||
val pageNum = _pageNum.asStateFlow()
|
||||
private val _pageNum = MutableStateFlow(0)
|
||||
val pageNum = _pageNum.asStateFlow()
|
||||
|
||||
private val _hasNextPage = MutableStateFlow(true)
|
||||
val hasNextPage = _hasNextPage.asStateFlow()
|
||||
private val _hasNextPage = MutableStateFlow(true)
|
||||
val hasNextPage = _hasNextPage.asStateFlow()
|
||||
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
|
||||
fun loadNextPage() {
|
||||
launch {
|
||||
if (hasNextPage.value && sourceMutex.tryLock()) {
|
||||
_pageNum.value++
|
||||
val page = fetcher.get(_pageNum.value)
|
||||
if (page != null) {
|
||||
_sourceManga.value = _sourceManga.value + page.mangaList
|
||||
_hasNextPage.value = page.hasNextPage
|
||||
} else {
|
||||
_pageNum.value--
|
||||
}
|
||||
sourceMutex.unlock()
|
||||
fun loadNextPage() {
|
||||
launch {
|
||||
if (hasNextPage.value && sourceMutex.tryLock()) {
|
||||
_pageNum.value++
|
||||
val page = fetcher.get(_pageNum.value)
|
||||
if (page != null) {
|
||||
_sourceManga.value = _sourceManga.value + page.mangaList
|
||||
_hasNextPage.value = page.hasNextPage
|
||||
} else {
|
||||
_pageNum.value--
|
||||
}
|
||||
_loading.value = false
|
||||
sourceMutex.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
_loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ enum class StartScreen {
|
||||
Library,
|
||||
Updates,
|
||||
|
||||
// History,
|
||||
// History,
|
||||
Sources,
|
||||
Extensions,
|
||||
}
|
||||
|
||||
@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GetRecentUpdates
|
||||
@Inject
|
||||
constructor(
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
pageNum: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(pageNum)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get updates for page $pageNum" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class GetRecentUpdates(
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
pageNum: Int,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(pageNum)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get updates for page $pageNum" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateCategory
|
||||
@Inject
|
||||
constructor(
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update category ${category.name}(${category.id})" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(categoryId: Long) = updatesRepository.updateCategory(categoryId)
|
||||
|
||||
fun asFlow(category: Category) = updatesRepository.updateCategory(category.id)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
@Inject
|
||||
class UpdateCategory(
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
) {
|
||||
suspend fun await(
|
||||
categoryId: Long,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(categoryId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update category $categoryId" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
suspend fun await(
|
||||
category: Category,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(category)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update category ${category.name}(${category.id})" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow(categoryId: Long) = updatesRepository.updateCategory(categoryId)
|
||||
|
||||
fun asFlow(category: Category) = updatesRepository.updateCategory(category.id)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,77 +21,76 @@ import kotlinx.coroutines.flow.singleOrNull
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateChecker
|
||||
@Inject
|
||||
constructor(
|
||||
private val updatePreferences: UpdatePreferences,
|
||||
private val client: Http,
|
||||
) {
|
||||
suspend fun await(
|
||||
manualFetch: Boolean,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manualFetch)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to check for updates" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(manualFetch: Boolean) =
|
||||
flow {
|
||||
if (!manualFetch && !updatePreferences.enabled().get()) return@flow
|
||||
val latestRelease = client.get(
|
||||
"https://api.github.com/repos/$GITHUB_REPO/releases/latest",
|
||||
).body<GithubRelease>()
|
||||
|
||||
if (isNewVersion(latestRelease.version)) {
|
||||
emit(Update.UpdateFound(latestRelease))
|
||||
} else {
|
||||
emit(Update.NoUpdatesFound)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
sealed class Update {
|
||||
data class UpdateFound(
|
||||
val release: GithubRelease,
|
||||
) : Update()
|
||||
|
||||
data object NoUpdatesFound : Update()
|
||||
@Inject
|
||||
class UpdateChecker(
|
||||
private val updatePreferences: UpdatePreferences,
|
||||
private val client: Http,
|
||||
) {
|
||||
suspend fun await(
|
||||
manualFetch: Boolean,
|
||||
onError: suspend (Throwable) -> Unit = {},
|
||||
) = asFlow(manualFetch)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to check for updates" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
// Thanks to Tachiyomi for inspiration
|
||||
private fun isNewVersion(versionTag: String): Boolean {
|
||||
// Removes prefixes like "r" or "v"
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
fun asFlow(manualFetch: Boolean) =
|
||||
flow {
|
||||
if (!manualFetch && !updatePreferences.enabled().get()) return@flow
|
||||
val latestRelease = client.get(
|
||||
"https://api.github.com/repos/$GITHUB_REPO/releases/latest",
|
||||
).body<GithubRelease>()
|
||||
|
||||
return if (BuildKonfig.IS_PREVIEW) {
|
||||
// Preview builds: based on releases in "Suwayomi/Suwayomi-JUI-preview" repo
|
||||
// tagged as something like "r123"
|
||||
newVersion.toInt() > BuildKonfig.PREVIEW_BUILD
|
||||
if (isNewVersion(latestRelease.version)) {
|
||||
emit(Update.UpdateFound(latestRelease))
|
||||
} else {
|
||||
// Release builds: based on releases in "Suwayomi/Suwayomi-JUI" repo
|
||||
// tagged as something like "v1.1.2"
|
||||
newVersion != BuildKonfig.VERSION
|
||||
emit(Update.NoUpdatesFound)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
companion object {
|
||||
private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) {
|
||||
"Suwayomi/Suwayomi-JUI-preview"
|
||||
} else {
|
||||
"Suwayomi/Suwayomi-JUI"
|
||||
}
|
||||
sealed class Update {
|
||||
data class UpdateFound(
|
||||
val release: GithubRelease,
|
||||
) : Update()
|
||||
|
||||
private val RELEASE_TAG: String by lazy {
|
||||
if (BuildKonfig.IS_PREVIEW) {
|
||||
"r${BuildKonfig.PREVIEW_BUILD}"
|
||||
} else {
|
||||
"v${BuildKonfig.VERSION}"
|
||||
}
|
||||
}
|
||||
data object NoUpdatesFound : Update()
|
||||
}
|
||||
|
||||
val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG"
|
||||
// Thanks to Tachiyomi for inspiration
|
||||
private fun isNewVersion(versionTag: String): Boolean {
|
||||
// Removes prefixes like "r" or "v"
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
private val log = logging()
|
||||
return if (BuildKonfig.IS_PREVIEW) {
|
||||
// Preview builds: based on releases in "Suwayomi/Suwayomi-JUI-preview" repo
|
||||
// tagged as something like "r123"
|
||||
newVersion.toInt() > BuildKonfig.PREVIEW_BUILD
|
||||
} else {
|
||||
// Release builds: based on releases in "Suwayomi/Suwayomi-JUI" repo
|
||||
// tagged as something like "v1.1.2"
|
||||
newVersion != BuildKonfig.VERSION
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) {
|
||||
"Suwayomi/Suwayomi-JUI-preview"
|
||||
} else {
|
||||
"Suwayomi/Suwayomi-JUI"
|
||||
}
|
||||
|
||||
private val RELEASE_TAG: String by lazy {
|
||||
if (BuildKonfig.IS_PREVIEW) {
|
||||
"r${BuildKonfig.PREVIEW_BUILD}"
|
||||
} else {
|
||||
"v${BuildKonfig.VERSION}"
|
||||
}
|
||||
}
|
||||
|
||||
val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG"
|
||||
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdateLibrary
|
||||
@Inject
|
||||
constructor(
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update library" }
|
||||
}
|
||||
.collect()
|
||||
@Inject
|
||||
class UpdateLibrary(
|
||||
private val updatesRepository: UpdatesRepository,
|
||||
) {
|
||||
suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
|
||||
asFlow()
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to update library" }
|
||||
}
|
||||
.collect()
|
||||
|
||||
fun asFlow() = updatesRepository.updateLibrary()
|
||||
fun asFlow() = updatesRepository.updateLibrary()
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,54 +37,54 @@ import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class UpdatesPager
|
||||
@Inject
|
||||
constructor(
|
||||
private val getRecentUpdates: GetRecentUpdates,
|
||||
private val getManga: GetManga,
|
||||
private val getChapter: GetChapter,
|
||||
private val serverListeners: ServerListeners,
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||
private val updatesMutex = Mutex()
|
||||
@Inject
|
||||
class UpdatesPager(
|
||||
private val getRecentUpdates: GetRecentUpdates,
|
||||
private val getManga: GetManga,
|
||||
private val getChapter: GetChapter,
|
||||
private val serverListeners: ServerListeners,
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
|
||||
private val updatesMutex = Mutex()
|
||||
|
||||
private val fetchedUpdates = MutableSharedFlow<List<MangaAndChapter>>()
|
||||
private val foldedUpdates = fetchedUpdates.runningFold(emptyList<Updates>()) { updates, newUpdates ->
|
||||
updates.ifEmpty {
|
||||
val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates
|
||||
listOf(
|
||||
Updates.Date(
|
||||
Instant.fromEpochSeconds(first.fetchedAt)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.date,
|
||||
),
|
||||
)
|
||||
} + newUpdates.fold(emptyList()) { list, (manga, chapter) ->
|
||||
val date = (list.lastOrNull() as? Updates.Update)?.let {
|
||||
val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt)
|
||||
private val fetchedUpdates = MutableSharedFlow<List<MangaAndChapter>>()
|
||||
private val foldedUpdates = fetchedUpdates.runningFold(emptyList<Updates>()) { updates, newUpdates ->
|
||||
updates.ifEmpty {
|
||||
val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates
|
||||
listOf(
|
||||
Updates.Date(
|
||||
Instant.fromEpochSeconds(first.fetchedAt)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.date
|
||||
val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.date
|
||||
chapterDate.takeUnless { it == lastUpdateDate }
|
||||
}
|
||||
|
||||
if (date == null) {
|
||||
list + Updates.Update(manga, chapter)
|
||||
} else {
|
||||
list + Updates.Date(date) + Updates.Update(manga, chapter)
|
||||
}
|
||||
.date,
|
||||
),
|
||||
)
|
||||
} + newUpdates.fold(emptyList()) { list, (manga, chapter) ->
|
||||
val date = (list.lastOrNull() as? Updates.Update)?.let {
|
||||
val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.date
|
||||
val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt)
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
.date
|
||||
chapterDate.takeUnless { it == lastUpdateDate }
|
||||
}
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val mangaIds = foldedUpdates.map { updates ->
|
||||
updates.filterIsInstance<Updates.Update>().map { it.manga.id }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
private val chapterIds = foldedUpdates.map { updates ->
|
||||
updates.filterIsInstance<Updates.Update>().map { it.chapter.id }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
if (date == null) {
|
||||
list + Updates.Update(manga, chapter)
|
||||
} else {
|
||||
list + Updates.Date(date) + Updates.Update(manga, chapter)
|
||||
}
|
||||
}
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val changedManga = serverListeners.mangaListener.runningFold(emptyMap<Long, Manga>()) { manga, updatedMangaIds ->
|
||||
private val mangaIds = foldedUpdates.map { updates ->
|
||||
updates.filterIsInstance<Updates.Update>().map { it.manga.id }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
private val chapterIds = foldedUpdates.map { updates ->
|
||||
updates.filterIsInstance<Updates.Update>().map { it.chapter.id }
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val changedManga =
|
||||
serverListeners.mangaListener.runningFold(emptyMap<Long, Manga>()) { manga, updatedMangaIds ->
|
||||
coroutineScope {
|
||||
manga + updatedMangaIds.filter { it in mangaIds.value }.map {
|
||||
async {
|
||||
@@ -94,82 +94,82 @@ class UpdatesPager
|
||||
}
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
private val changedChapters = MutableStateFlow(emptyMap<Long, Chapter>())
|
||||
private val changedChapters = MutableStateFlow(emptyMap<Long, Chapter>())
|
||||
|
||||
init {
|
||||
serverListeners.chapterIdsListener
|
||||
.onEach { updatedChapterIds ->
|
||||
val chapters = coroutineScope {
|
||||
updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it == id } }.map {
|
||||
async {
|
||||
getChapter.await(it)
|
||||
}
|
||||
}.awaitAll().filterNotNull().associateBy { it.id }
|
||||
}
|
||||
changedChapters.update { it + chapters }
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
val updates = combine(
|
||||
foldedUpdates,
|
||||
changedManga,
|
||||
changedChapters,
|
||||
) { updates, changedManga, changedChapters ->
|
||||
updates.map {
|
||||
when (it) {
|
||||
is Updates.Date -> it
|
||||
|
||||
is Updates.Update -> it.copy(
|
||||
manga = changedManga[it.manga.id] ?: it.manga,
|
||||
chapter = changedChapters[it.chapter.id] ?: it.chapter,
|
||||
)
|
||||
init {
|
||||
serverListeners.chapterIdsListener
|
||||
.onEach { updatedChapterIds ->
|
||||
val chapters = coroutineScope {
|
||||
updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it == id } }.map {
|
||||
async {
|
||||
getChapter.await(it)
|
||||
}
|
||||
}.awaitAll().filterNotNull().associateBy { it.id }
|
||||
}
|
||||
changedChapters.update { it + chapters }
|
||||
}
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private val currentPage = MutableStateFlow(0)
|
||||
private val hasNextPage = MutableStateFlow(true)
|
||||
val updates = combine(
|
||||
foldedUpdates,
|
||||
changedManga,
|
||||
changedChapters,
|
||||
) { updates, changedManga, changedChapters ->
|
||||
updates.map {
|
||||
when (it) {
|
||||
is Updates.Date -> it
|
||||
|
||||
is Updates.Update -> it.copy(
|
||||
manga = changedManga[it.manga.id] ?: it.manga,
|
||||
chapter = changedChapters[it.chapter.id] ?: it.chapter,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(this, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private val currentPage = MutableStateFlow(0)
|
||||
private val hasNextPage = MutableStateFlow(true)
|
||||
|
||||
@Immutable
|
||||
sealed class Updates {
|
||||
@Immutable
|
||||
data class Update(
|
||||
val manga: Manga,
|
||||
val chapter: Chapter,
|
||||
) : Updates()
|
||||
|
||||
@Immutable
|
||||
sealed class Updates {
|
||||
@Immutable
|
||||
data class Update(
|
||||
val manga: Manga,
|
||||
val chapter: Chapter,
|
||||
) : Updates()
|
||||
|
||||
@Immutable
|
||||
data class Date(
|
||||
val date: String,
|
||||
) : Updates() {
|
||||
constructor(date: LocalDate) : this(date.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextPage(
|
||||
onComplete: (() -> Unit)? = null,
|
||||
onError: suspend (Throwable) -> Unit,
|
||||
) {
|
||||
launch {
|
||||
if (hasNextPage.value && updatesMutex.tryLock()) {
|
||||
currentPage.value++
|
||||
if (!getUpdates(currentPage.value, onError)) {
|
||||
currentPage.value--
|
||||
}
|
||||
updatesMutex.unlock()
|
||||
}
|
||||
onComplete?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUpdates(
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit,
|
||||
): Boolean {
|
||||
val updates = getRecentUpdates.await(page, onError) ?: return false
|
||||
hasNextPage.value = updates.hasNextPage
|
||||
fetchedUpdates.emit(updates.page)
|
||||
return true
|
||||
data class Date(
|
||||
val date: String,
|
||||
) : Updates() {
|
||||
constructor(date: LocalDate) : this(date.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextPage(
|
||||
onComplete: (() -> Unit)? = null,
|
||||
onError: suspend (Throwable) -> Unit,
|
||||
) {
|
||||
launch {
|
||||
if (hasNextPage.value && updatesMutex.tryLock()) {
|
||||
currentPage.value++
|
||||
if (!getUpdates(currentPage.value, onError)) {
|
||||
currentPage.value--
|
||||
}
|
||||
updatesMutex.unlock()
|
||||
}
|
||||
onComplete?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUpdates(
|
||||
page: Int,
|
||||
onError: suspend (Throwable) -> Unit,
|
||||
): Boolean {
|
||||
val updates = getRecentUpdates.await(page, onError) ?: return false
|
||||
hasNextPage.value = updates.hasNextPage
|
||||
fetchedUpdates.emit(updates.page)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,169 +38,168 @@ import kotlin.io.path.exists
|
||||
import kotlin.io.path.isExecutable
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class ServerService
|
||||
@Inject
|
||||
constructor(
|
||||
private val serverHostPreferences: ServerHostPreferences,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
@Inject
|
||||
class ServerService(
|
||||
private val serverHostPreferences: ServerHostPreferences,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val host = serverHostPreferences.host().stateIn(GlobalScope)
|
||||
private val _initialized = MutableStateFlow(
|
||||
if (host.value) {
|
||||
ServerResult.STARTING
|
||||
} else {
|
||||
ServerResult.UNUSED
|
||||
},
|
||||
)
|
||||
val initialized = _initialized.asStateFlow()
|
||||
private var process: Process? = null
|
||||
private val host = serverHostPreferences.host().stateIn(GlobalScope)
|
||||
private val _initialized = MutableStateFlow(
|
||||
if (host.value) {
|
||||
ServerResult.STARTING
|
||||
} else {
|
||||
ServerResult.UNUSED
|
||||
},
|
||||
)
|
||||
val initialized = _initialized.asStateFlow()
|
||||
private var process: Process? = null
|
||||
|
||||
fun startAnyway() {
|
||||
_initialized.value = ServerResult.UNUSED
|
||||
}
|
||||
fun startAnyway() {
|
||||
_initialized.value = ServerResult.UNUSED
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private suspend fun copyJar(jarFile: Path) {
|
||||
javaClass.getResourceAsStream("/Tachidesk.jar")?.source()
|
||||
?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer())
|
||||
}
|
||||
@Throws(IOException::class)
|
||||
private suspend fun copyJar(jarFile: Path) {
|
||||
javaClass.getResourceAsStream("/Tachidesk.jar")?.source()
|
||||
?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer())
|
||||
}
|
||||
|
||||
private fun getJavaFromPath(javaPath: Path): String? {
|
||||
val javaExeFile = javaPath.resolve("java.exe").toNioPath()
|
||||
val javaUnixFile = javaPath.resolve("java").toNioPath()
|
||||
return when {
|
||||
javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString()
|
||||
javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRuntimeJava(): String? = System.getProperty("java.home")?.let { getJavaFromPath(it.toPath().resolve("bin")) }
|
||||
|
||||
private fun getPossibleJava(): String? =
|
||||
System.getProperty("java.library.path")?.split(pathSeparatorChar)
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
val file = it.toPath()
|
||||
if (file.toString().contains("java") || file.toString().contains("jdk")) {
|
||||
if (file.name.equals("bin", true)) {
|
||||
file
|
||||
} else {
|
||||
file.resolve("bin")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.mapNotNull { getJavaFromPath(it) }
|
||||
.firstOrNull()
|
||||
|
||||
private suspend fun runService() {
|
||||
process?.destroy()
|
||||
withIOContext {
|
||||
process?.waitFor()
|
||||
}
|
||||
_initialized.value = if (host.value) {
|
||||
ServerResult.STARTING
|
||||
} else {
|
||||
ServerResult.UNUSED
|
||||
return
|
||||
}
|
||||
|
||||
val jarFile = userDataDir / "Tachidesk.jar"
|
||||
if (!FileSystem.SYSTEM.exists(jarFile)) {
|
||||
log.info { "Copying server to resources" }
|
||||
withIOContext { copyJar(jarFile) }
|
||||
} else {
|
||||
try {
|
||||
val jarVersion = withIOContext {
|
||||
JarInputStream(FileSystem.SYSTEM.source(jarFile).buffer().inputStream()).use { jar ->
|
||||
jar.manifest?.mainAttributes?.getValue("JUI-KEY")?.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
if (jarVersion != BuildKonfig.SERVER_CODE) {
|
||||
log.info { "Updating server file from resources" }
|
||||
withIOContext { copyJar(jarFile) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error(e) {
|
||||
"Error accessing server jar, cannot update server, ${BuildKonfig.NAME} may not work properly"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val javaPath = getRuntimeJava() ?: getPossibleJava() ?: "java"
|
||||
log.info { "Starting server with $javaPath" }
|
||||
val properties = serverHostPreferences.properties()
|
||||
log.info { "Using server properties:\n" + properties.joinToString(separator = "\n") }
|
||||
|
||||
withIOContext {
|
||||
val reader: Reader
|
||||
process = ProcessBuilder(javaPath, *properties, "-jar", jarFile.toString())
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
.also {
|
||||
reader = it.inputStream.reader()
|
||||
}
|
||||
log.info { "Server started successfully" }
|
||||
val log = logging("Server")
|
||||
reader.forEachLine {
|
||||
if (_initialized.value == ServerResult.STARTING) {
|
||||
when {
|
||||
it.contains("Javalin started") ->
|
||||
_initialized.value = ServerResult.STARTED
|
||||
|
||||
it.contains("Javalin has stopped") ->
|
||||
_initialized.value = ServerResult.FAILED
|
||||
}
|
||||
}
|
||||
log.info { it }
|
||||
}
|
||||
if (_initialized.value == ServerResult.STARTING) {
|
||||
_initialized.value = ServerResult.FAILED
|
||||
}
|
||||
log.info { "Server closed" }
|
||||
val exitVal = process?.waitFor()
|
||||
log.info { "Process exitValue: $exitVal" }
|
||||
process = null
|
||||
}
|
||||
}
|
||||
|
||||
fun startServer() {
|
||||
scope.coroutineContext.cancelChildren()
|
||||
host
|
||||
.mapLatest {
|
||||
runService()
|
||||
}
|
||||
.catch {
|
||||
log.error(it) { "Error launching Tachidesk.jar" }
|
||||
if (_initialized.value == ServerResult.STARTING || _initialized.value == ServerResult.STARTED) {
|
||||
_initialized.value = ServerResult.FAILED
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
init {
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
thread(start = false) {
|
||||
process?.destroy()
|
||||
process = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum class ServerResult {
|
||||
UNUSED,
|
||||
STARTING,
|
||||
STARTED,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
private fun getJavaFromPath(javaPath: Path): String? {
|
||||
val javaExeFile = javaPath.resolve("java.exe").toNioPath()
|
||||
val javaUnixFile = javaPath.resolve("java").toNioPath()
|
||||
return when {
|
||||
javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString()
|
||||
javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRuntimeJava(): String? = System.getProperty("java.home")?.let { getJavaFromPath(it.toPath().resolve("bin")) }
|
||||
|
||||
private fun getPossibleJava(): String? =
|
||||
System.getProperty("java.library.path")?.split(pathSeparatorChar)
|
||||
.orEmpty()
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
val file = it.toPath()
|
||||
if (file.toString().contains("java") || file.toString().contains("jdk")) {
|
||||
if (file.name.equals("bin", true)) {
|
||||
file
|
||||
} else {
|
||||
file.resolve("bin")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.mapNotNull { getJavaFromPath(it) }
|
||||
.firstOrNull()
|
||||
|
||||
private suspend fun runService() {
|
||||
process?.destroy()
|
||||
withIOContext {
|
||||
process?.waitFor()
|
||||
}
|
||||
_initialized.value = if (host.value) {
|
||||
ServerResult.STARTING
|
||||
} else {
|
||||
ServerResult.UNUSED
|
||||
return
|
||||
}
|
||||
|
||||
val jarFile = userDataDir / "Tachidesk.jar"
|
||||
if (!FileSystem.SYSTEM.exists(jarFile)) {
|
||||
log.info { "Copying server to resources" }
|
||||
withIOContext { copyJar(jarFile) }
|
||||
} else {
|
||||
try {
|
||||
val jarVersion = withIOContext {
|
||||
JarInputStream(FileSystem.SYSTEM.source(jarFile).buffer().inputStream()).use { jar ->
|
||||
jar.manifest?.mainAttributes?.getValue("JUI-KEY")?.toIntOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
if (jarVersion != BuildKonfig.SERVER_CODE) {
|
||||
log.info { "Updating server file from resources" }
|
||||
withIOContext { copyJar(jarFile) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
log.error(e) {
|
||||
"Error accessing server jar, cannot update server, ${BuildKonfig.NAME} may not work properly"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val javaPath = getRuntimeJava() ?: getPossibleJava() ?: "java"
|
||||
log.info { "Starting server with $javaPath" }
|
||||
val properties = serverHostPreferences.properties()
|
||||
log.info { "Using server properties:\n" + properties.joinToString(separator = "\n") }
|
||||
|
||||
withIOContext {
|
||||
val reader: Reader
|
||||
process = ProcessBuilder(javaPath, *properties, "-jar", jarFile.toString())
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
.also {
|
||||
reader = it.inputStream.reader()
|
||||
}
|
||||
log.info { "Server started successfully" }
|
||||
val log = logging("Server")
|
||||
reader.forEachLine {
|
||||
if (_initialized.value == ServerResult.STARTING) {
|
||||
when {
|
||||
it.contains("Javalin started") ->
|
||||
_initialized.value = ServerResult.STARTED
|
||||
|
||||
it.contains("Javalin has stopped") ->
|
||||
_initialized.value = ServerResult.FAILED
|
||||
}
|
||||
}
|
||||
log.info { it }
|
||||
}
|
||||
if (_initialized.value == ServerResult.STARTING) {
|
||||
_initialized.value = ServerResult.FAILED
|
||||
}
|
||||
log.info { "Server closed" }
|
||||
val exitVal = process?.waitFor()
|
||||
log.info { "Process exitValue: $exitVal" }
|
||||
process = null
|
||||
}
|
||||
}
|
||||
|
||||
fun startServer() {
|
||||
scope.coroutineContext.cancelChildren()
|
||||
host
|
||||
.mapLatest {
|
||||
runService()
|
||||
}
|
||||
.catch {
|
||||
log.error(it) { "Error launching Tachidesk.jar" }
|
||||
if (_initialized.value == ServerResult.STARTING || _initialized.value == ServerResult.STARTED) {
|
||||
_initialized.value = ServerResult.FAILED
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
init {
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
thread(start = false) {
|
||||
process?.destroy()
|
||||
process = null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum class ServerResult {
|
||||
UNUSED,
|
||||
STARTING,
|
||||
STARTED,
|
||||
FAILED,
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import me.tatarka.inject.annotations.Inject
|
||||
@Composable
|
||||
actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {}
|
||||
|
||||
actual class SettingsServerHostViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper)
|
||||
@Inject
|
||||
actual class SettingsServerHostViewModel(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper)
|
||||
|
||||
@@ -30,104 +30,103 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class ImageLoaderProvider
|
||||
@Inject
|
||||
constructor(
|
||||
private val http: Http,
|
||||
serverPreferences: ServerPreferences,
|
||||
private val context: ContextWrapper,
|
||||
) {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
|
||||
@Inject
|
||||
class ImageLoaderProvider(
|
||||
private val http: Http,
|
||||
serverPreferences: ServerPreferences,
|
||||
private val context: ContextWrapper,
|
||||
) {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
|
||||
|
||||
fun get(imageCache: ImageCache): ImageLoader =
|
||||
ImageLoader {
|
||||
components {
|
||||
register(context, http)
|
||||
add(MokoResourceFetcher.Factory())
|
||||
add(MangaCoverMapper())
|
||||
add(MangaCoverKeyer())
|
||||
add(ExtensionIconMapper())
|
||||
add(ExtensionIconKeyer())
|
||||
add(SourceIconMapper())
|
||||
add(SourceIconKeyer())
|
||||
}
|
||||
options {
|
||||
configure(context)
|
||||
}
|
||||
interceptor {
|
||||
diskCache { imageCache }
|
||||
bitmapMemoryCacheConfig { configure(context) }
|
||||
}
|
||||
fun get(imageCache: ImageCache): ImageLoader =
|
||||
ImageLoader {
|
||||
components {
|
||||
register(context, http)
|
||||
add(MokoResourceFetcher.Factory())
|
||||
add(MangaCoverMapper())
|
||||
add(MangaCoverKeyer())
|
||||
add(ExtensionIconMapper())
|
||||
add(ExtensionIconKeyer())
|
||||
add(SourceIconMapper())
|
||||
add(SourceIconKeyer())
|
||||
}
|
||||
|
||||
inner class MangaCoverMapper : Mapper<Url> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options,
|
||||
): Url? {
|
||||
if (data !is Manga) return null
|
||||
if (data.thumbnailUrl.isNullOrBlank()) return null
|
||||
return Url(serverUrl.value.toString() + data.thumbnailUrl)
|
||||
options {
|
||||
configure(context)
|
||||
}
|
||||
interceptor {
|
||||
diskCache { imageCache }
|
||||
bitmapMemoryCacheConfig { configure(context) }
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCoverKeyer : Keyer {
|
||||
override fun key(
|
||||
data: Any,
|
||||
options: Options,
|
||||
type: Keyer.Type,
|
||||
): String? {
|
||||
if (data !is Manga) return null
|
||||
return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}"
|
||||
}
|
||||
}
|
||||
|
||||
inner class ExtensionIconMapper : Mapper<Url> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options,
|
||||
): Url? {
|
||||
if (data !is Extension) return null
|
||||
if (data.iconUrl.isBlank()) return null
|
||||
return Url("${serverUrl.value}${data.iconUrl}")
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionIconKeyer : Keyer {
|
||||
override fun key(
|
||||
data: Any,
|
||||
options: Options,
|
||||
type: Keyer.Type,
|
||||
): String? {
|
||||
if (data !is Extension) return null
|
||||
return data.iconUrl
|
||||
}
|
||||
}
|
||||
|
||||
inner class SourceIconMapper : Mapper<Url> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options,
|
||||
): Url? {
|
||||
if (data !is Source) return null
|
||||
if (data.iconUrl.isBlank()) return null
|
||||
return Url(serverUrl.value.toString() + data.iconUrl)
|
||||
}
|
||||
}
|
||||
|
||||
class SourceIconKeyer : Keyer {
|
||||
override fun key(
|
||||
data: Any,
|
||||
options: Options,
|
||||
type: Keyer.Type,
|
||||
): String? {
|
||||
if (data !is Source) return null
|
||||
return data.iconUrl
|
||||
}
|
||||
inner class MangaCoverMapper : Mapper<Url> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options,
|
||||
): Url? {
|
||||
if (data !is Manga) return null
|
||||
if (data.thumbnailUrl.isNullOrBlank()) return null
|
||||
return Url(serverUrl.value.toString() + data.thumbnailUrl)
|
||||
}
|
||||
}
|
||||
|
||||
class MangaCoverKeyer : Keyer {
|
||||
override fun key(
|
||||
data: Any,
|
||||
options: Options,
|
||||
type: Keyer.Type,
|
||||
): String? {
|
||||
if (data !is Manga) return null
|
||||
return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}"
|
||||
}
|
||||
}
|
||||
|
||||
inner class ExtensionIconMapper : Mapper<Url> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options,
|
||||
): Url? {
|
||||
if (data !is Extension) return null
|
||||
if (data.iconUrl.isBlank()) return null
|
||||
return Url("${serverUrl.value}${data.iconUrl}")
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionIconKeyer : Keyer {
|
||||
override fun key(
|
||||
data: Any,
|
||||
options: Options,
|
||||
type: Keyer.Type,
|
||||
): String? {
|
||||
if (data !is Extension) return null
|
||||
return data.iconUrl
|
||||
}
|
||||
}
|
||||
|
||||
inner class SourceIconMapper : Mapper<Url> {
|
||||
override fun map(
|
||||
data: Any,
|
||||
options: Options,
|
||||
): Url? {
|
||||
if (data !is Source) return null
|
||||
if (data.iconUrl.isBlank()) return null
|
||||
return Url(serverUrl.value.toString() + data.iconUrl)
|
||||
}
|
||||
}
|
||||
|
||||
class SourceIconKeyer : Keyer {
|
||||
override fun key(
|
||||
data: Any,
|
||||
options: Options,
|
||||
type: Keyer.Type,
|
||||
): String? {
|
||||
if (data !is Source) return null
|
||||
return data.iconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect fun OptionsBuilder.configure(contextWrapper: ContextWrapper)
|
||||
|
||||
expect fun ComponentRegistryBuilder.register(
|
||||
|
||||
@@ -59,100 +59,102 @@ fun AppTheme(content: @Composable () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
class AppThemeViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
@Inject
|
||||
class AppThemeViewModel(
|
||||
private val uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
|
||||
private val themeMode = uiPreferences.themeMode().asStateFlow()
|
||||
private val lightTheme = uiPreferences.lightTheme().asStateFlow()
|
||||
private val darkTheme = uiPreferences.darkTheme().asStateFlow()
|
||||
private val themeMode = uiPreferences.themeMode().asStateFlow()
|
||||
private val lightTheme = uiPreferences.lightTheme().asStateFlow()
|
||||
private val darkTheme = uiPreferences.darkTheme().asStateFlow()
|
||||
|
||||
private val baseThemeJob = SupervisorJob()
|
||||
private val baseThemeScope = CoroutineScope(baseThemeJob)
|
||||
private val baseThemeJob = SupervisorJob()
|
||||
private val baseThemeScope = CoroutineScope(baseThemeJob)
|
||||
|
||||
@Composable
|
||||
fun getColors(): Pair<Colors, ExtraColors> {
|
||||
val themeMode by themeMode.collectAsState()
|
||||
val lightTheme by lightTheme.collectAsState()
|
||||
val darkTheme by darkTheme.collectAsState()
|
||||
@Composable
|
||||
fun getColors(): Pair<Colors, ExtraColors> {
|
||||
val themeMode by themeMode.collectAsState()
|
||||
val lightTheme by lightTheme.collectAsState()
|
||||
val darkTheme by darkTheme.collectAsState()
|
||||
|
||||
val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme)
|
||||
val colors = remember(baseTheme.colors.isLight) {
|
||||
baseThemeJob.cancelChildren()
|
||||
val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme)
|
||||
val colors = remember(baseTheme.colors.isLight) {
|
||||
baseThemeJob.cancelChildren()
|
||||
|
||||
if (baseTheme.colors.isLight) {
|
||||
uiPreferences.getLightColors().asStateFlow(baseThemeScope)
|
||||
} else {
|
||||
uiPreferences.getDarkColors().asStateFlow(baseThemeScope)
|
||||
}
|
||||
}
|
||||
|
||||
val primary by colors.primaryStateFlow.collectAsState()
|
||||
val secondary by colors.secondaryStateFlow.collectAsState()
|
||||
val tertiary by colors.tertiaryStateFlow.collectAsState()
|
||||
|
||||
return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors(baseTheme.extraColors, tertiary)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBaseTheme(
|
||||
themeMode: ThemeMode,
|
||||
lightTheme: Int,
|
||||
darkTheme: Int,
|
||||
): Theme {
|
||||
fun getTheme(
|
||||
id: Int,
|
||||
isLight: Boolean,
|
||||
): Theme =
|
||||
themes.find { it.id == id && it.colors.isLight == isLight }
|
||||
?: themes.first { it.colors.isLight == isLight }
|
||||
|
||||
return when (themeMode) {
|
||||
ThemeMode.System -> if (!isSystemInDarkTheme()) {
|
||||
getTheme(lightTheme, true)
|
||||
} else {
|
||||
getTheme(darkTheme, false)
|
||||
}
|
||||
|
||||
ThemeMode.Light -> getTheme(lightTheme, true)
|
||||
|
||||
ThemeMode.Dark -> getTheme(darkTheme, false)
|
||||
if (baseTheme.colors.isLight) {
|
||||
uiPreferences.getLightColors().asStateFlow(baseThemeScope)
|
||||
} else {
|
||||
uiPreferences.getDarkColors().asStateFlow(baseThemeScope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaterialColors(
|
||||
baseColors: Colors,
|
||||
colorPrimary: Color,
|
||||
colorSecondary: Color,
|
||||
): Colors {
|
||||
val primary = colorPrimary.takeOrElse { baseColors.primary }
|
||||
val secondary = colorSecondary.takeOrElse { baseColors.secondary }
|
||||
return baseColors.copy(
|
||||
primary = primary,
|
||||
primaryVariant = primary,
|
||||
secondary = secondary,
|
||||
secondaryVariant = secondary,
|
||||
onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White,
|
||||
onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White,
|
||||
)
|
||||
}
|
||||
val primary by colors.primaryStateFlow.collectAsState()
|
||||
val secondary by colors.secondaryStateFlow.collectAsState()
|
||||
val tertiary by colors.tertiaryStateFlow.collectAsState()
|
||||
|
||||
private fun getExtraColors(
|
||||
baseExtraColors: ExtraColors,
|
||||
colorTertiary: Color,
|
||||
): ExtraColors {
|
||||
val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary }
|
||||
return baseExtraColors.copy(
|
||||
tertiary = tertiary,
|
||||
)
|
||||
}
|
||||
return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors(
|
||||
baseTheme.extraColors,
|
||||
tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
baseThemeScope.cancel()
|
||||
scope.cancel()
|
||||
@Composable
|
||||
private fun getBaseTheme(
|
||||
themeMode: ThemeMode,
|
||||
lightTheme: Int,
|
||||
darkTheme: Int,
|
||||
): Theme {
|
||||
fun getTheme(
|
||||
id: Int,
|
||||
isLight: Boolean,
|
||||
): Theme =
|
||||
themes.find { it.id == id && it.colors.isLight == isLight }
|
||||
?: themes.first { it.colors.isLight == isLight }
|
||||
|
||||
return when (themeMode) {
|
||||
ThemeMode.System -> if (!isSystemInDarkTheme()) {
|
||||
getTheme(lightTheme, true)
|
||||
} else {
|
||||
getTheme(darkTheme, false)
|
||||
}
|
||||
|
||||
ThemeMode.Light -> getTheme(lightTheme, true)
|
||||
|
||||
ThemeMode.Dark -> getTheme(darkTheme, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaterialColors(
|
||||
baseColors: Colors,
|
||||
colorPrimary: Color,
|
||||
colorSecondary: Color,
|
||||
): Colors {
|
||||
val primary = colorPrimary.takeOrElse { baseColors.primary }
|
||||
val secondary = colorSecondary.takeOrElse { baseColors.secondary }
|
||||
return baseColors.copy(
|
||||
primary = primary,
|
||||
primaryVariant = primary,
|
||||
secondary = secondary,
|
||||
secondaryVariant = secondary,
|
||||
onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White,
|
||||
onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExtraColors(
|
||||
baseExtraColors: ExtraColors,
|
||||
colorTertiary: Color,
|
||||
): ExtraColors {
|
||||
val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary }
|
||||
return baseExtraColors.copy(
|
||||
tertiary = tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
baseThemeScope.cancel()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,121 +27,126 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class CategoriesScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getCategories: GetCategories,
|
||||
private val createCategory: CreateCategory,
|
||||
private val deleteCategory: DeleteCategory,
|
||||
private val modifyCategory: ModifyCategory,
|
||||
private val reorderCategory: ReorderCategory,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private var originalCategories = emptyList<Category>()
|
||||
private val _categories = MutableStateFlow<ImmutableList<MenuCategory>>(persistentListOf())
|
||||
val categories = _categories.asStateFlow()
|
||||
@Inject
|
||||
class CategoriesScreenViewModel(
|
||||
private val getCategories: GetCategories,
|
||||
private val createCategory: CreateCategory,
|
||||
private val deleteCategory: DeleteCategory,
|
||||
private val modifyCategory: ModifyCategory,
|
||||
private val reorderCategory: ReorderCategory,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private var originalCategories = emptyList<Category>()
|
||||
private val _categories = MutableStateFlow<ImmutableList<MenuCategory>>(persistentListOf())
|
||||
val categories = _categories.asStateFlow()
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
getCategories()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCategories() {
|
||||
_categories.value = persistentListOf()
|
||||
val categories = getCategories.await(true, onError = { toast(it.message.orEmpty()) })
|
||||
if (categories != null) {
|
||||
_categories.value = categories
|
||||
.sortedBy { it.order }
|
||||
.also { originalCategories = it }
|
||||
.map { it.toMenuCategory() }
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateRemoteCategories(manualUpdate: Boolean = false) {
|
||||
val categories = _categories.value
|
||||
val newCategories = categories.filter { it.id == null }
|
||||
newCategories.forEach {
|
||||
createCategory.await(it.name, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
originalCategories.forEach { originalCategory ->
|
||||
val category = categories.find { it.id == originalCategory.id }
|
||||
if (category == null) {
|
||||
deleteCategory.await(originalCategory, onError = { toast(it.message.orEmpty()) })
|
||||
} else if (category.name != originalCategory.name) {
|
||||
modifyCategory.await(originalCategory, category.name, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
var updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) })
|
||||
categories.sortedBy { it.order }.forEach { category ->
|
||||
val updatedCategory = updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach
|
||||
if (category.order != updatedCategory.order) {
|
||||
log.debug { "${category.name}: ${updatedCategory.order} to ${category.order}" }
|
||||
reorderCategory.await(category.id!!, category.order, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
|
||||
if (manualUpdate) {
|
||||
getCategories()
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCategory(
|
||||
category: MenuCategory,
|
||||
newName: String,
|
||||
) {
|
||||
_categories.value = (_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun deleteCategory(category: MenuCategory) {
|
||||
_categories.value = _categories.value.toPersistentList() - category
|
||||
}
|
||||
|
||||
fun createCategory(name: String) {
|
||||
_categories.value =
|
||||
_categories.value.toPersistentList() + MenuCategory(order = categories.value.size + 1, name = name, default = false)
|
||||
}
|
||||
|
||||
fun moveUp(category: MenuCategory) {
|
||||
val categories = _categories.value.toMutableList()
|
||||
val index = categories.indexOf(category)
|
||||
if (index == -1) throw Exception("Invalid index")
|
||||
categories.add(index - 1, categories.removeAt(index))
|
||||
_categories.value = categories
|
||||
.mapIndexed { i, menuCategory ->
|
||||
menuCategory.copy(order = i + 1)
|
||||
}
|
||||
.sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun moveDown(category: MenuCategory) {
|
||||
val categories = _categories.value.toMutableList()
|
||||
val index = categories.indexOf(category)
|
||||
if (index == -1) throw Exception("Invalid index")
|
||||
categories.add(index + 1, categories.removeAt(index))
|
||||
_categories.value = categories
|
||||
.mapIndexed { i, menuCategory ->
|
||||
menuCategory.copy(order = i + 1)
|
||||
}
|
||||
.sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun Category.toMenuCategory() = MenuCategory(id, order, name, default)
|
||||
|
||||
@Stable
|
||||
data class MenuCategory(
|
||||
val id: Long? = null,
|
||||
val order: Int,
|
||||
val name: String,
|
||||
val default: Boolean = false,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
init {
|
||||
scope.launch {
|
||||
getCategories()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCategories() {
|
||||
_categories.value = persistentListOf()
|
||||
val categories = getCategories.await(true, onError = { toast(it.message.orEmpty()) })
|
||||
if (categories != null) {
|
||||
_categories.value = categories
|
||||
.sortedBy { it.order }
|
||||
.also { originalCategories = it }
|
||||
.map { it.toMenuCategory() }
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateRemoteCategories(manualUpdate: Boolean = false) {
|
||||
val categories = _categories.value
|
||||
val newCategories = categories.filter { it.id == null }
|
||||
newCategories.forEach {
|
||||
createCategory.await(it.name, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
originalCategories.forEach { originalCategory ->
|
||||
val category = categories.find { it.id == originalCategory.id }
|
||||
if (category == null) {
|
||||
deleteCategory.await(originalCategory, onError = { toast(it.message.orEmpty()) })
|
||||
} else if (category.name != originalCategory.name) {
|
||||
modifyCategory.await(originalCategory, category.name, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
var updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) })
|
||||
categories.sortedBy { it.order }.forEach { category ->
|
||||
val updatedCategory =
|
||||
updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach
|
||||
if (category.order != updatedCategory.order) {
|
||||
log.debug { "${category.name}: ${updatedCategory.order} to ${category.order}" }
|
||||
reorderCategory.await(category.id!!, category.order, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
|
||||
if (manualUpdate) {
|
||||
getCategories()
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCategory(
|
||||
category: MenuCategory,
|
||||
newName: String,
|
||||
) {
|
||||
_categories.value =
|
||||
(_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun deleteCategory(category: MenuCategory) {
|
||||
_categories.value = _categories.value.toPersistentList() - category
|
||||
}
|
||||
|
||||
fun createCategory(name: String) {
|
||||
_categories.value =
|
||||
_categories.value.toPersistentList() + MenuCategory(
|
||||
order = categories.value.size + 1,
|
||||
name = name,
|
||||
default = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun moveUp(category: MenuCategory) {
|
||||
val categories = _categories.value.toMutableList()
|
||||
val index = categories.indexOf(category)
|
||||
if (index == -1) throw Exception("Invalid index")
|
||||
categories.add(index - 1, categories.removeAt(index))
|
||||
_categories.value = categories
|
||||
.mapIndexed { i, menuCategory ->
|
||||
menuCategory.copy(order = i + 1)
|
||||
}
|
||||
.sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun moveDown(category: MenuCategory) {
|
||||
val categories = _categories.value.toMutableList()
|
||||
val index = categories.indexOf(category)
|
||||
if (index == -1) throw Exception("Invalid index")
|
||||
categories.add(index + 1, categories.removeAt(index))
|
||||
_categories.value = categories
|
||||
.mapIndexed { i, menuCategory ->
|
||||
menuCategory.copy(order = i + 1)
|
||||
}
|
||||
.sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun Category.toMenuCategory() = MenuCategory(id, order, name, default)
|
||||
|
||||
@Stable
|
||||
data class MenuCategory(
|
||||
val id: Long? = null,
|
||||
val order: Int,
|
||||
val name: String,
|
||||
val default: Boolean = false,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,85 +31,90 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class DownloadsScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val downloadService: DownloadService,
|
||||
private val startDownloading: StartDownloading,
|
||||
private val stopDownloading: StopDownloading,
|
||||
private val clearDownloadQueue: ClearDownloadQueue,
|
||||
private val queueChapterDownload: QueueChapterDownload,
|
||||
private val stopChapterDownload: StopChapterDownload,
|
||||
private val reorderChapterDownload: ReorderChapterDownload,
|
||||
private val contextWrapper: ContextWrapper,
|
||||
@Assisted standalone: Boolean,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val uiScope = if (standalone) {
|
||||
MainScope()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@Inject
|
||||
class DownloadsScreenViewModel(
|
||||
private val downloadService: DownloadService,
|
||||
private val startDownloading: StartDownloading,
|
||||
private val stopDownloading: StopDownloading,
|
||||
private val clearDownloadQueue: ClearDownloadQueue,
|
||||
private val queueChapterDownload: QueueChapterDownload,
|
||||
private val stopChapterDownload: StopChapterDownload,
|
||||
private val reorderChapterDownload: ReorderChapterDownload,
|
||||
private val contextWrapper: ContextWrapper,
|
||||
@Assisted standalone: Boolean,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val uiScope = if (standalone) {
|
||||
MainScope()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override val scope: CoroutineScope
|
||||
get() = uiScope ?: super.scope
|
||||
override val scope: CoroutineScope
|
||||
get() = uiScope ?: super.scope
|
||||
|
||||
val serviceStatus = DownloadService.status.asStateFlow()
|
||||
val downloaderStatus = DownloadService.downloaderStatus.asStateFlow()
|
||||
val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val serviceStatus = DownloadService.status.asStateFlow()
|
||||
val downloaderStatus = DownloadService.downloaderStatus.asStateFlow()
|
||||
val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
fun start() {
|
||||
scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
fun start() {
|
||||
scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
fun pause() {
|
||||
scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
fun clear() {
|
||||
scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun stopDownload(chapter: Chapter) {
|
||||
scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
fun stopDownload(chapter: Chapter) {
|
||||
scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun moveUp(chapter: Chapter) {
|
||||
scope.launch {
|
||||
val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index }
|
||||
if (index == -1 || index <= 0) return@launch
|
||||
reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun moveDown(chapter: Chapter) {
|
||||
scope.launch {
|
||||
val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index }
|
||||
if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch
|
||||
reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToTop(chapter: Chapter) {
|
||||
scope.launch {
|
||||
reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToBottom(chapter: Chapter) {
|
||||
scope.launch {
|
||||
reorderChapterDownload.await(chapter.id, downloadQueue.value.lastIndex, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART)
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
uiScope?.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
fun moveUp(chapter: Chapter) {
|
||||
scope.launch {
|
||||
val index =
|
||||
downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index }
|
||||
if (index == -1 || index <= 0) return@launch
|
||||
reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun moveDown(chapter: Chapter) {
|
||||
scope.launch {
|
||||
val index =
|
||||
downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index }
|
||||
if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch
|
||||
reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToTop(chapter: Chapter) {
|
||||
scope.launch {
|
||||
reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToBottom(chapter: Chapter) {
|
||||
scope.launch {
|
||||
reorderChapterDownload.await(
|
||||
chapter.id,
|
||||
downloadQueue.value.lastIndex,
|
||||
onError = { toast(it.message.orEmpty()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART)
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
uiScope?.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,188 +41,187 @@ import okio.Source
|
||||
import org.lighthousegames.logging.logging
|
||||
import kotlin.random.Random
|
||||
|
||||
class ExtensionsScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getExtensionList: GetExtensionList,
|
||||
private val installExtensionFile: InstallExtensionFile,
|
||||
private val installExtension: InstallExtension,
|
||||
private val updateExtension: UpdateExtension,
|
||||
private val uninstallExtension: UninstallExtension,
|
||||
extensionPreferences: ExtensionPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val extensionList = MutableStateFlow<List<Extension>?>(null)
|
||||
@Inject
|
||||
class ExtensionsScreenViewModel(
|
||||
private val getExtensionList: GetExtensionList,
|
||||
private val installExtensionFile: InstallExtensionFile,
|
||||
private val installExtension: InstallExtension,
|
||||
private val updateExtension: UpdateExtension,
|
||||
private val uninstallExtension: UninstallExtension,
|
||||
extensionPreferences: ExtensionPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val extensionList = MutableStateFlow<List<Extension>?>(null)
|
||||
|
||||
private val _enabledLangs = extensionPreferences.languages().asStateFlow()
|
||||
val enabledLangs = _enabledLangs.map { it.toImmutableSet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
|
||||
private val _enabledLangs = extensionPreferences.languages().asStateFlow()
|
||||
val enabledLangs = _enabledLangs.map { it.toImmutableSet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
|
||||
|
||||
private val _searchQuery = MutableStateFlow<String?>(null)
|
||||
val searchQuery = _searchQuery.asStateFlow()
|
||||
private val _searchQuery = MutableStateFlow<String?>(null)
|
||||
val searchQuery = _searchQuery.asStateFlow()
|
||||
|
||||
private val workingExtensions = MutableStateFlow<List<String>>(emptyList())
|
||||
private val workingExtensions = MutableStateFlow<List<String>>(emptyList())
|
||||
|
||||
val extensions = combine(
|
||||
searchQuery,
|
||||
extensionList,
|
||||
enabledLangs,
|
||||
workingExtensions,
|
||||
) { searchQuery, extensions, enabledLangs, workingExtensions ->
|
||||
search(searchQuery, extensions, enabledLangs, workingExtensions)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val extensions = combine(
|
||||
searchQuery,
|
||||
extensionList,
|
||||
enabledLangs,
|
||||
workingExtensions,
|
||||
) { searchQuery, extensions, enabledLangs, workingExtensions ->
|
||||
search(searchQuery, extensions, enabledLangs, workingExtensions)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val availableLangs = extensionList.filterNotNull().map { langs ->
|
||||
langs.map { it.lang }.distinct().toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val availableLangs = extensionList.filterNotNull().map { langs ->
|
||||
langs.map { it.lang }.distinct().toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val isLoading = extensionList.map { it == null }.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
val isLoading = extensionList.map { it == null }.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
getExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getExtensions() {
|
||||
extensionList.value = getExtensionList.await(onError = { toast(it.message.orEmpty()) }).orEmpty()
|
||||
}
|
||||
|
||||
fun install(source: Source) {
|
||||
log.info { "Install file clicked" }
|
||||
scope.launch {
|
||||
try {
|
||||
val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
|
||||
.resolve("tachidesk.${Random.nextLong()}.apk")
|
||||
.also { file ->
|
||||
source.saveTo(file)
|
||||
}
|
||||
installExtensionFile.await(file, onError = { toast(it.message.orEmpty()) })
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error creating apk file" }
|
||||
// todo toast if error
|
||||
e.throwIfCancellation()
|
||||
}
|
||||
|
||||
getExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
fun install(extension: Extension) {
|
||||
log.info { "Install clicked" }
|
||||
scope.launch {
|
||||
workingExtensions.update { it + extension.apkName }
|
||||
installExtension.await(extension, onError = { toast(it.message.orEmpty()) })
|
||||
getExtensions()
|
||||
workingExtensions.update { it - extension.apkName }
|
||||
}
|
||||
}
|
||||
|
||||
fun update(extension: Extension) {
|
||||
log.info { "Update clicked" }
|
||||
scope.launch {
|
||||
workingExtensions.update { it + extension.apkName }
|
||||
updateExtension.await(extension, onError = { toast(it.message.orEmpty()) })
|
||||
getExtensions()
|
||||
workingExtensions.update { it - extension.apkName }
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall(extension: Extension) {
|
||||
log.info { "Uninstall clicked" }
|
||||
scope.launch {
|
||||
workingExtensions.update { it + extension.apkName }
|
||||
uninstallExtension.await(extension, onError = { toast(it.message.orEmpty()) })
|
||||
getExtensions()
|
||||
workingExtensions.update { it - extension.apkName }
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnabledLanguages(langs: Set<String>) {
|
||||
_enabledLangs.value = langs
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
private fun search(
|
||||
searchQuery: String?,
|
||||
extensionList: List<Extension>?,
|
||||
enabledLangs: Set<String>,
|
||||
workingExtensions: List<String>,
|
||||
): ImmutableList<ExtensionUI> {
|
||||
val extensions = extensionList?.filter { it.lang in enabledLangs }
|
||||
.orEmpty()
|
||||
return if (searchQuery.isNullOrBlank()) {
|
||||
extensions.splitSort(workingExtensions)
|
||||
} else {
|
||||
val queries = searchQuery.split(" ")
|
||||
val filteredExtensions = extensions.toMutableList()
|
||||
queries.forEach { query ->
|
||||
filteredExtensions.removeAll { !it.name.contains(query, true) }
|
||||
}
|
||||
filteredExtensions.toList().splitSort(workingExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Extension>.splitSort(workingExtensions: List<String>): ImmutableList<ExtensionUI> {
|
||||
val all = MR.strings.all.toPlatformString()
|
||||
return this
|
||||
.filter(Extension::installed)
|
||||
.sortedWith(
|
||||
compareBy<Extension> {
|
||||
when {
|
||||
it.obsolete -> 1
|
||||
it.hasUpdate -> 2
|
||||
else -> 3
|
||||
}
|
||||
}
|
||||
.thenBy(Extension::lang)
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER, Extension::name),
|
||||
)
|
||||
.map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) }
|
||||
.let {
|
||||
if (it.isNotEmpty()) {
|
||||
listOf(ExtensionUI.Header(MR.strings.installed.toPlatformString())) + it
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.plus(
|
||||
filterNot(Extension::installed)
|
||||
.groupBy(Extension::lang)
|
||||
.mapKeys { (key) ->
|
||||
when (key) {
|
||||
"all" -> all
|
||||
else -> Locale(key).displayName
|
||||
}
|
||||
}
|
||||
.mapValues {
|
||||
it.value
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Extension::name))
|
||||
.map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) }
|
||||
}
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareBy<Pair<String, *>> { (key) ->
|
||||
when (key) {
|
||||
all -> 1
|
||||
else -> 2
|
||||
}
|
||||
}.thenBy(String.CASE_INSENSITIVE_ORDER, Pair<String, *>::first),
|
||||
)
|
||||
.flatMap { (key, value) ->
|
||||
listOf(ExtensionUI.Header(key)) + value
|
||||
},
|
||||
)
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
init {
|
||||
scope.launch {
|
||||
getExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getExtensions() {
|
||||
extensionList.value = getExtensionList.await(onError = { toast(it.message.orEmpty()) }).orEmpty()
|
||||
}
|
||||
|
||||
fun install(source: Source) {
|
||||
log.info { "Install file clicked" }
|
||||
scope.launch {
|
||||
try {
|
||||
val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY
|
||||
.resolve("tachidesk.${Random.nextLong()}.apk")
|
||||
.also { file ->
|
||||
source.saveTo(file)
|
||||
}
|
||||
installExtensionFile.await(file, onError = { toast(it.message.orEmpty()) })
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error creating apk file" }
|
||||
// todo toast if error
|
||||
e.throwIfCancellation()
|
||||
}
|
||||
|
||||
getExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
fun install(extension: Extension) {
|
||||
log.info { "Install clicked" }
|
||||
scope.launch {
|
||||
workingExtensions.update { it + extension.apkName }
|
||||
installExtension.await(extension, onError = { toast(it.message.orEmpty()) })
|
||||
getExtensions()
|
||||
workingExtensions.update { it - extension.apkName }
|
||||
}
|
||||
}
|
||||
|
||||
fun update(extension: Extension) {
|
||||
log.info { "Update clicked" }
|
||||
scope.launch {
|
||||
workingExtensions.update { it + extension.apkName }
|
||||
updateExtension.await(extension, onError = { toast(it.message.orEmpty()) })
|
||||
getExtensions()
|
||||
workingExtensions.update { it - extension.apkName }
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall(extension: Extension) {
|
||||
log.info { "Uninstall clicked" }
|
||||
scope.launch {
|
||||
workingExtensions.update { it + extension.apkName }
|
||||
uninstallExtension.await(extension, onError = { toast(it.message.orEmpty()) })
|
||||
getExtensions()
|
||||
workingExtensions.update { it - extension.apkName }
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnabledLanguages(langs: Set<String>) {
|
||||
_enabledLangs.value = langs
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
private fun search(
|
||||
searchQuery: String?,
|
||||
extensionList: List<Extension>?,
|
||||
enabledLangs: Set<String>,
|
||||
workingExtensions: List<String>,
|
||||
): ImmutableList<ExtensionUI> {
|
||||
val extensions = extensionList?.filter { it.lang in enabledLangs }
|
||||
.orEmpty()
|
||||
return if (searchQuery.isNullOrBlank()) {
|
||||
extensions.splitSort(workingExtensions)
|
||||
} else {
|
||||
val queries = searchQuery.split(" ")
|
||||
val filteredExtensions = extensions.toMutableList()
|
||||
queries.forEach { query ->
|
||||
filteredExtensions.removeAll { !it.name.contains(query, true) }
|
||||
}
|
||||
filteredExtensions.toList().splitSort(workingExtensions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Extension>.splitSort(workingExtensions: List<String>): ImmutableList<ExtensionUI> {
|
||||
val all = MR.strings.all.toPlatformString()
|
||||
return this
|
||||
.filter(Extension::installed)
|
||||
.sortedWith(
|
||||
compareBy<Extension> {
|
||||
when {
|
||||
it.obsolete -> 1
|
||||
it.hasUpdate -> 2
|
||||
else -> 3
|
||||
}
|
||||
}
|
||||
.thenBy(Extension::lang)
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER, Extension::name),
|
||||
)
|
||||
.map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) }
|
||||
.let {
|
||||
if (it.isNotEmpty()) {
|
||||
listOf(ExtensionUI.Header(MR.strings.installed.toPlatformString())) + it
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.plus(
|
||||
filterNot(Extension::installed)
|
||||
.groupBy(Extension::lang)
|
||||
.mapKeys { (key) ->
|
||||
when (key) {
|
||||
"all" -> all
|
||||
else -> Locale(key).displayName
|
||||
}
|
||||
}
|
||||
.mapValues {
|
||||
it.value
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Extension::name))
|
||||
.map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) }
|
||||
}
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareBy<Pair<String, *>> { (key) ->
|
||||
when (key) {
|
||||
all -> 1
|
||||
else -> 2
|
||||
}
|
||||
}.thenBy(String.CASE_INSENSITIVE_ORDER, Pair<String, *>::first),
|
||||
)
|
||||
.flatMap { (key, value) ->
|
||||
listOf(ExtensionUI.Header(key)) + value
|
||||
},
|
||||
)
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class ExtensionUI {
|
||||
data class Header(
|
||||
|
||||
@@ -119,224 +119,223 @@ private fun LibraryMap.setManga(
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getCategories: GetCategories,
|
||||
private val getMangaListFromCategory: GetMangaListFromCategory,
|
||||
private val removeMangaFromLibrary: RemoveMangaFromLibrary,
|
||||
private val updateLibrary: UpdateLibrary,
|
||||
private val updateCategory: UpdateCategory,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf())
|
||||
val categories = library.categories.asStateFlow()
|
||||
@Inject
|
||||
class LibraryScreenViewModel(
|
||||
private val getCategories: GetCategories,
|
||||
private val getMangaListFromCategory: GetMangaListFromCategory,
|
||||
private val removeMangaFromLibrary: RemoveMangaFromLibrary,
|
||||
private val updateLibrary: UpdateLibrary,
|
||||
private val updateCategory: UpdateCategory,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf())
|
||||
val categories = library.categories.asStateFlow()
|
||||
|
||||
private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 }
|
||||
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
|
||||
private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 }
|
||||
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
|
||||
|
||||
private val _showingMenu by savedStateHandle.getStateFlow { false }
|
||||
val showingMenu = _showingMenu.asStateFlow()
|
||||
private val _showingMenu by savedStateHandle.getStateFlow { false }
|
||||
val showingMenu = _showingMenu.asStateFlow()
|
||||
|
||||
val displayMode = libraryPreferences.displayMode().stateIn(scope)
|
||||
val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
|
||||
val gridSize = libraryPreferences.gridSize().stateIn(scope)
|
||||
val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope)
|
||||
val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope)
|
||||
val languageBadges = libraryPreferences.languageBadge().stateIn(scope)
|
||||
val localBadges = libraryPreferences.localBadge().stateIn(scope)
|
||||
val displayMode = libraryPreferences.displayMode().stateIn(scope)
|
||||
val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
|
||||
val gridSize = libraryPreferences.gridSize().stateIn(scope)
|
||||
val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope)
|
||||
val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope)
|
||||
val languageBadges = libraryPreferences.languageBadge().stateIn(scope)
|
||||
val localBadges = libraryPreferences.localBadge().stateIn(scope)
|
||||
|
||||
private val sortMode = libraryPreferences.sortMode().stateIn(scope)
|
||||
private val sortAscending = libraryPreferences.sortAscending().stateIn(scope)
|
||||
private val sortMode = libraryPreferences.sortMode().stateIn(scope)
|
||||
private val sortAscending = libraryPreferences.sortAscending().stateIn(scope)
|
||||
|
||||
private val filter: Flow<(Manga) -> Boolean> = combine(
|
||||
libraryPreferences.filterDownloaded().getAsFlow(),
|
||||
libraryPreferences.filterUnread().getAsFlow(),
|
||||
libraryPreferences.filterCompleted().getAsFlow(),
|
||||
) { downloaded, unread, completed ->
|
||||
{ manga ->
|
||||
when (downloaded) {
|
||||
FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0
|
||||
FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0
|
||||
private val filter: Flow<(Manga) -> Boolean> = combine(
|
||||
libraryPreferences.filterDownloaded().getAsFlow(),
|
||||
libraryPreferences.filterUnread().getAsFlow(),
|
||||
libraryPreferences.filterCompleted().getAsFlow(),
|
||||
) { downloaded, unread, completed ->
|
||||
{ manga ->
|
||||
when (downloaded) {
|
||||
FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0
|
||||
FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0
|
||||
FilterState.IGNORED -> true
|
||||
} &&
|
||||
when (unread) {
|
||||
FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0
|
||||
FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0
|
||||
FilterState.IGNORED -> true
|
||||
} &&
|
||||
when (unread) {
|
||||
FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0
|
||||
FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0
|
||||
FilterState.IGNORED -> true
|
||||
} &&
|
||||
when (completed) {
|
||||
FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED
|
||||
FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED
|
||||
FilterState.IGNORED -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _query by savedStateHandle.getStateFlow { "" }
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending ->
|
||||
getComparator(sortMode, sortAscending)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title })
|
||||
|
||||
init {
|
||||
getLibrary()
|
||||
}
|
||||
|
||||
private fun getLibrary() {
|
||||
library.categories.value = LibraryState.Loading
|
||||
getCategories.asFlow()
|
||||
.onEach { categories ->
|
||||
if (categories.isEmpty()) {
|
||||
throw Exception(MR.strings.library_empty.toPlatformString())
|
||||
}
|
||||
library.categories.value = LibraryState.Loaded(
|
||||
categories.sortedBy { it.order }
|
||||
.toImmutableList(),
|
||||
)
|
||||
categories.forEach { category ->
|
||||
getMangaListFromCategory.asFlow(category)
|
||||
.onEach {
|
||||
library.mangaMap.setManga(
|
||||
id = category.id,
|
||||
manga = it.toImmutableList(),
|
||||
getItemsFlow = ::getMangaItemsFlow,
|
||||
)
|
||||
}
|
||||
.catch {
|
||||
log.warn(it) { "Failed to get manga list from category ${category.name}" }
|
||||
library.mangaMap.setError(category.id, it)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
when (completed) {
|
||||
FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED
|
||||
FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED
|
||||
FilterState.IGNORED -> true
|
||||
}
|
||||
.catch {
|
||||
library.categories.value = LibraryState.Failed(it)
|
||||
log.warn(it) { "Failed to get categories" }
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setSelectedPage(page: Int) {
|
||||
_selectedCategoryIndex.value = page
|
||||
}
|
||||
|
||||
fun setShowingMenu(showingMenu: Boolean) {
|
||||
_showingMenu.value = showingMenu
|
||||
}
|
||||
|
||||
private fun getComparator(
|
||||
sortMode: Sort,
|
||||
ascending: Boolean,
|
||||
): Comparator<Manga> {
|
||||
val sortFn: (Manga, Manga) -> Int = when (sortMode) {
|
||||
Sort.ALPHABETICAL -> {
|
||||
val locale = Locale.current
|
||||
val collator = CollatorComparator(locale);
|
||||
|
||||
{ a, b ->
|
||||
collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale))
|
||||
}
|
||||
}
|
||||
|
||||
Sort.UNREAD -> {
|
||||
{ a, b ->
|
||||
when {
|
||||
// Ensure unread content comes first
|
||||
(a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0
|
||||
|
||||
a.unreadCount == null || a.unreadCount == 0 -> if (ascending) 1 else -1
|
||||
|
||||
b.unreadCount == null || b.unreadCount == 0 -> if (ascending) -1 else 1
|
||||
|
||||
else -> (a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Sort.DATE_ADDED -> {
|
||||
{ a, b ->
|
||||
a.inLibraryAt.compareTo(b.inLibraryAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (ascending) {
|
||||
Comparator(sortFn)
|
||||
} else {
|
||||
Comparator(sortFn).reversed()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun filterManga(
|
||||
query: String,
|
||||
mangaList: List<Manga>,
|
||||
): List<Manga> {
|
||||
if (query.isBlank()) return mangaList
|
||||
val queries = query.split(" ")
|
||||
return mangaList.asFlow()
|
||||
.filter { manga ->
|
||||
queries.all { query ->
|
||||
manga.title.contains(query, true) ||
|
||||
manga.author.orEmpty().contains(query, true) ||
|
||||
manga.artist.orEmpty().contains(query, true) ||
|
||||
manga.genre.any { it.contains(query, true) } ||
|
||||
manga.description.orEmpty().contains(query, true) ||
|
||||
manga.status.name.contains(query, true)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
.buffer()
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<Manga>>): StateFlow<ImmutableList<Manga>> =
|
||||
combine(
|
||||
unfilteredItemsFlow,
|
||||
query,
|
||||
) {
|
||||
unfilteredItems,
|
||||
query,
|
||||
->
|
||||
filterManga(query, unfilteredItems)
|
||||
}.combine(filter) { filteredManga, filterer ->
|
||||
filteredManga.filter(filterer)
|
||||
}.combine(comparator) { filteredManga, comparator ->
|
||||
filteredManga.sortedWith(comparator)
|
||||
}.map {
|
||||
it.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
fun getLibraryForCategoryId(id: Long): StateFlow<CategoryState> = library.mangaMap.getManga(id)
|
||||
|
||||
private fun getCategoriesToUpdate(mangaId: Long): List<Category> =
|
||||
library.mangaMap
|
||||
.filter { mangaMapEntry ->
|
||||
(mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null
|
||||
}
|
||||
.mapNotNull { (id) -> (library.categories.value as? LibraryState.Loaded)?.categories?.first { it.id == id } }
|
||||
|
||||
fun removeManga(mangaId: Long) {
|
||||
scope.launch {
|
||||
removeMangaFromLibrary.await(mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
fun updateLibrary() {
|
||||
scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun updateCategory(category: Category) {
|
||||
scope.launch { updateCategory.await(category, onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
private val _query by savedStateHandle.getStateFlow { "" }
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending ->
|
||||
getComparator(sortMode, sortAscending)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title })
|
||||
|
||||
init {
|
||||
getLibrary()
|
||||
}
|
||||
|
||||
private fun getLibrary() {
|
||||
library.categories.value = LibraryState.Loading
|
||||
getCategories.asFlow()
|
||||
.onEach { categories ->
|
||||
if (categories.isEmpty()) {
|
||||
throw Exception(MR.strings.library_empty.toPlatformString())
|
||||
}
|
||||
library.categories.value = LibraryState.Loaded(
|
||||
categories.sortedBy { it.order }
|
||||
.toImmutableList(),
|
||||
)
|
||||
categories.forEach { category ->
|
||||
getMangaListFromCategory.asFlow(category)
|
||||
.onEach {
|
||||
library.mangaMap.setManga(
|
||||
id = category.id,
|
||||
manga = it.toImmutableList(),
|
||||
getItemsFlow = ::getMangaItemsFlow,
|
||||
)
|
||||
}
|
||||
.catch {
|
||||
log.warn(it) { "Failed to get manga list from category ${category.name}" }
|
||||
library.mangaMap.setError(category.id, it)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
library.categories.value = LibraryState.Failed(it)
|
||||
log.warn(it) { "Failed to get categories" }
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setSelectedPage(page: Int) {
|
||||
_selectedCategoryIndex.value = page
|
||||
}
|
||||
|
||||
fun setShowingMenu(showingMenu: Boolean) {
|
||||
_showingMenu.value = showingMenu
|
||||
}
|
||||
|
||||
private fun getComparator(
|
||||
sortMode: Sort,
|
||||
ascending: Boolean,
|
||||
): Comparator<Manga> {
|
||||
val sortFn: (Manga, Manga) -> Int = when (sortMode) {
|
||||
Sort.ALPHABETICAL -> {
|
||||
val locale = Locale.current
|
||||
val collator = CollatorComparator(locale);
|
||||
|
||||
{ a, b ->
|
||||
collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale))
|
||||
}
|
||||
}
|
||||
|
||||
Sort.UNREAD -> {
|
||||
{ a, b ->
|
||||
when {
|
||||
// Ensure unread content comes first
|
||||
(a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0
|
||||
|
||||
a.unreadCount == null || a.unreadCount == 0 -> if (ascending) 1 else -1
|
||||
|
||||
b.unreadCount == null || b.unreadCount == 0 -> if (ascending) -1 else 1
|
||||
|
||||
else -> (a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Sort.DATE_ADDED -> {
|
||||
{ a, b ->
|
||||
a.inLibraryAt.compareTo(b.inLibraryAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (ascending) {
|
||||
Comparator(sortFn)
|
||||
} else {
|
||||
Comparator(sortFn).reversed()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun filterManga(
|
||||
query: String,
|
||||
mangaList: List<Manga>,
|
||||
): List<Manga> {
|
||||
if (query.isBlank()) return mangaList
|
||||
val queries = query.split(" ")
|
||||
return mangaList.asFlow()
|
||||
.filter { manga ->
|
||||
queries.all { query ->
|
||||
manga.title.contains(query, true) ||
|
||||
manga.author.orEmpty().contains(query, true) ||
|
||||
manga.artist.orEmpty().contains(query, true) ||
|
||||
manga.genre.any { it.contains(query, true) } ||
|
||||
manga.description.orEmpty().contains(query, true) ||
|
||||
manga.status.name.contains(query, true)
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
.buffer()
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<Manga>>): StateFlow<ImmutableList<Manga>> =
|
||||
combine(
|
||||
unfilteredItemsFlow,
|
||||
query,
|
||||
) {
|
||||
unfilteredItems,
|
||||
query,
|
||||
->
|
||||
filterManga(query, unfilteredItems)
|
||||
}.combine(filter) { filteredManga, filterer ->
|
||||
filteredManga.filter(filterer)
|
||||
}.combine(comparator) { filteredManga, comparator ->
|
||||
filteredManga.sortedWith(comparator)
|
||||
}.map {
|
||||
it.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
fun getLibraryForCategoryId(id: Long): StateFlow<CategoryState> = library.mangaMap.getManga(id)
|
||||
|
||||
private fun getCategoriesToUpdate(mangaId: Long): List<Category> =
|
||||
library.mangaMap
|
||||
.filter { mangaMapEntry ->
|
||||
(mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null
|
||||
}
|
||||
.mapNotNull { (id) -> (library.categories.value as? LibraryState.Loaded)?.categories?.first { it.id == id } }
|
||||
|
||||
fun removeManga(mangaId: Long) {
|
||||
scope.launch {
|
||||
removeMangaFromLibrary.await(mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
fun updateLibrary() {
|
||||
scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun updateCategory(category: Category) {
|
||||
scope.launch { updateCategory.await(category, onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,21 @@ import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class LibrarySettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow()
|
||||
val filterUnread = libraryPreferences.filterUnread().asStateFlow()
|
||||
val filterCompleted = libraryPreferences.filterCompleted().asStateFlow()
|
||||
@Inject
|
||||
class LibrarySettingsViewModel(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow()
|
||||
val filterUnread = libraryPreferences.filterUnread().asStateFlow()
|
||||
val filterCompleted = libraryPreferences.filterCompleted().asStateFlow()
|
||||
|
||||
val sortMode = libraryPreferences.sortMode().asStateFlow()
|
||||
val sortAscending = libraryPreferences.sortAscending().asStateFlow()
|
||||
val sortMode = libraryPreferences.sortMode().asStateFlow()
|
||||
val sortAscending = libraryPreferences.sortAscending().asStateFlow()
|
||||
|
||||
val displayMode = libraryPreferences.displayMode().asStateFlow()
|
||||
val unreadBadges = libraryPreferences.unreadBadge().asStateFlow()
|
||||
val downloadBadges = libraryPreferences.downloadBadge().asStateFlow()
|
||||
val languageBadges = libraryPreferences.languageBadge().asStateFlow()
|
||||
val localBadges = libraryPreferences.localBadge().asStateFlow()
|
||||
}
|
||||
val displayMode = libraryPreferences.displayMode().asStateFlow()
|
||||
val unreadBadges = libraryPreferences.unreadBadge().asStateFlow()
|
||||
val downloadBadges = libraryPreferences.downloadBadge().asStateFlow()
|
||||
val languageBadges = libraryPreferences.languageBadge().asStateFlow()
|
||||
val localBadges = libraryPreferences.localBadge().asStateFlow()
|
||||
}
|
||||
|
||||
@@ -14,23 +14,22 @@ import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
@Inject
|
||||
class MainViewModel(
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
|
||||
val startScreen = uiPreferences.startScreen().get()
|
||||
val confirmExit = uiPreferences.confirmExit().stateIn(scope)
|
||||
val startScreen = uiPreferences.startScreen().get()
|
||||
val confirmExit = uiPreferences.confirmExit().stateIn(scope)
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
fun confirmExitToast() {
|
||||
toast(MR.strings.confirm_exit_toast.toPlatformString())
|
||||
}
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
fun confirmExitToast() {
|
||||
toast(MR.strings.confirm_exit_toast.toPlatformString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,49 +26,48 @@ import kotlinx.datetime.Instant
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class AboutViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val dateHandler: DateHandler,
|
||||
private val aboutServer: AboutServer,
|
||||
private val updateChecker: UpdateChecker,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _aboutHolder = MutableStateFlow<About?>(null)
|
||||
val aboutHolder = _aboutHolder.asStateFlow()
|
||||
@Inject
|
||||
class AboutViewModel(
|
||||
private val dateHandler: DateHandler,
|
||||
private val aboutServer: AboutServer,
|
||||
private val updateChecker: UpdateChecker,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _aboutHolder = MutableStateFlow<About?>(null)
|
||||
val aboutHolder = _aboutHolder.asStateFlow()
|
||||
|
||||
val formattedBuildTime = aboutHolder.map { about ->
|
||||
about ?: return@map ""
|
||||
getFormattedDate(Instant.fromEpochSeconds(about.buildTime))
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
val formattedBuildTime = aboutHolder.map { about ->
|
||||
about ?: return@map ""
|
||||
getFormattedDate(Instant.fromEpochSeconds(about.buildTime))
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
private val _updates = MutableSharedFlow<Update.UpdateFound>()
|
||||
val updates = _updates.asSharedFlow()
|
||||
private val _updates = MutableSharedFlow<Update.UpdateFound>()
|
||||
val updates = _updates.asSharedFlow()
|
||||
|
||||
init {
|
||||
getAbout()
|
||||
}
|
||||
init {
|
||||
getAbout()
|
||||
}
|
||||
|
||||
private fun getAbout() {
|
||||
scope.launch {
|
||||
_aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForUpdates() {
|
||||
scope.launch {
|
||||
toast(MR.strings.update_check_look_for_updates.toPlatformString())
|
||||
when (val update = updateChecker.await(true, onError = { toast(it.message.orEmpty()) })) {
|
||||
is Update.UpdateFound -> _updates.emit(update)
|
||||
is Update.NoUpdatesFound -> toast(MR.strings.update_check_no_new_updates.toPlatformString())
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedDate(time: Instant): String = dateHandler.dateTimeFormat(time)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
private fun getAbout() {
|
||||
scope.launch {
|
||||
_aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun checkForUpdates() {
|
||||
scope.launch {
|
||||
toast(MR.strings.update_check_look_for_updates.toPlatformString())
|
||||
when (val update = updateChecker.await(true, onError = { toast(it.message.orEmpty()) })) {
|
||||
is Update.UpdateFound -> _updates.emit(update)
|
||||
is Update.NoUpdatesFound -> toast(MR.strings.update_check_no_new_updates.toPlatformString())
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedDate(time: Instant): String = dateHandler.dateTimeFormat(time)
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,33 +18,32 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class LibraryUpdatesViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val libraryUpdateService: LibraryUpdateService,
|
||||
private val contextWrapper: ContextWrapper,
|
||||
@Assisted standalone: Boolean,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val uiScope = if (standalone) {
|
||||
MainScope()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override val scope: CoroutineScope
|
||||
get() = uiScope ?: super.scope
|
||||
|
||||
val serviceStatus = LibraryUpdateService.status.asStateFlow()
|
||||
val updateStatus = LibraryUpdateService.updateStatus.asStateFlow()
|
||||
|
||||
fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART)
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
uiScope?.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
@Inject
|
||||
class LibraryUpdatesViewModel(
|
||||
private val libraryUpdateService: LibraryUpdateService,
|
||||
private val contextWrapper: ContextWrapper,
|
||||
@Assisted standalone: Boolean,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val uiScope = if (standalone) {
|
||||
MainScope()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
override val scope: CoroutineScope
|
||||
get() = uiScope ?: super.scope
|
||||
|
||||
val serviceStatus = LibraryUpdateService.status.asStateFlow()
|
||||
val updateStatus = LibraryUpdateService.updateStatus.asStateFlow()
|
||||
|
||||
fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART)
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
uiScope?.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,386 +55,386 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class MangaScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val dateHandler: DateHandler,
|
||||
private val getManga: GetManga,
|
||||
private val refreshManga: RefreshManga,
|
||||
private val getChapters: GetChapters,
|
||||
private val refreshChapters: RefreshChapters,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val queueChapterDownload: QueueChapterDownload,
|
||||
private val stopChapterDownload: StopChapterDownload,
|
||||
private val deleteChapterDownload: DeleteChapterDownload,
|
||||
private val getCategories: GetCategories,
|
||||
private val getMangaCategories: GetMangaCategories,
|
||||
private val addMangaToCategory: AddMangaToCategory,
|
||||
private val removeMangaFromCategory: RemoveMangaFromCategory,
|
||||
private val addMangaToLibrary: AddMangaToLibrary,
|
||||
private val removeMangaFromLibrary: RemoveMangaFromLibrary,
|
||||
private val batchChapterDownload: BatchChapterDownload,
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _manga = MutableStateFlow<Manga?>(null)
|
||||
val manga = _manga.asStateFlow()
|
||||
@Inject
|
||||
class MangaScreenViewModel(
|
||||
private val dateHandler: DateHandler,
|
||||
private val getManga: GetManga,
|
||||
private val refreshManga: RefreshManga,
|
||||
private val getChapters: GetChapters,
|
||||
private val refreshChapters: RefreshChapters,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val queueChapterDownload: QueueChapterDownload,
|
||||
private val stopChapterDownload: StopChapterDownload,
|
||||
private val deleteChapterDownload: DeleteChapterDownload,
|
||||
private val getCategories: GetCategories,
|
||||
private val getMangaCategories: GetMangaCategories,
|
||||
private val addMangaToCategory: AddMangaToCategory,
|
||||
private val removeMangaFromCategory: RemoveMangaFromCategory,
|
||||
private val addMangaToLibrary: AddMangaToLibrary,
|
||||
private val removeMangaFromLibrary: RemoveMangaFromLibrary,
|
||||
private val batchChapterDownload: BatchChapterDownload,
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _manga = MutableStateFlow<Manga?>(null)
|
||||
val manga = _manga.asStateFlow()
|
||||
|
||||
private val _chapters = MutableStateFlow<ImmutableList<ChapterDownloadItem>>(persistentListOf())
|
||||
val chapters = _chapters.asStateFlow()
|
||||
private val _chapters = MutableStateFlow<ImmutableList<ChapterDownloadItem>>(persistentListOf())
|
||||
val chapters = _chapters.asStateFlow()
|
||||
|
||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||
val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems ->
|
||||
chapters.filter { it.isSelected(selecteditems) }.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||
val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems ->
|
||||
chapters.filter { it.isSelected(selecteditems) }.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val loadingManga = MutableStateFlow(true)
|
||||
private val loadingChapters = MutableStateFlow(true)
|
||||
private val refreshingChapters = MutableStateFlow(false)
|
||||
private val refreshingManga = MutableStateFlow(false)
|
||||
val isLoading = combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d }
|
||||
private val loadingManga = MutableStateFlow(true)
|
||||
private val loadingChapters = MutableStateFlow(true)
|
||||
private val refreshingChapters = MutableStateFlow(false)
|
||||
private val refreshingManga = MutableStateFlow(false)
|
||||
val isLoading =
|
||||
combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d }
|
||||
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
|
||||
val categories = getCategories.asFlow(true)
|
||||
.map { it.toImmutableList() }
|
||||
val categories = getCategories.asFlow(true)
|
||||
.map { it.toImmutableList() }
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Failed to get categories" }
|
||||
}
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _mangaCategories = MutableStateFlow<ImmutableList<Category>>(persistentListOf())
|
||||
val mangaCategories = _mangaCategories.asStateFlow()
|
||||
|
||||
val categoriesExist = categories.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
|
||||
val inActionMode = _selectedIds.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
private val chooseCategoriesFlow = MutableSharedFlow<Unit>()
|
||||
val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow())
|
||||
|
||||
private val reloadManga = MutableSharedFlow<Unit>()
|
||||
private val reloadChapters = MutableSharedFlow<Unit>()
|
||||
|
||||
val dateTimeFormatter = uiPreferences.dateFormat().changes()
|
||||
.map {
|
||||
dateHandler.getDateFormat(it)
|
||||
}
|
||||
.asStateFlow(dateHandler.getDateFormat(uiPreferences.dateFormat().get()))
|
||||
|
||||
init {
|
||||
DownloadService.registerWatch(params.mangaId)
|
||||
.mapLatest { downloadingChapters ->
|
||||
chapters.value.forEach { chapter ->
|
||||
chapter.updateFrom(downloadingChapters)
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
reloadManga.onStart { emit(Unit) }.flatMapLatest {
|
||||
loadingManga.value = true
|
||||
getManga.asFlow(params.mangaId)
|
||||
}
|
||||
.onEach {
|
||||
_manga.value = it
|
||||
loadingManga.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Failed to get categories" }
|
||||
log.warn(it) { "Error when loading manga" }
|
||||
loadingManga.value = false
|
||||
}
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
.launchIn(scope)
|
||||
|
||||
private val _mangaCategories = MutableStateFlow<ImmutableList<Category>>(persistentListOf())
|
||||
val mangaCategories = _mangaCategories.asStateFlow()
|
||||
|
||||
val categoriesExist = categories.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
|
||||
val inActionMode = _selectedIds.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
private val chooseCategoriesFlow = MutableSharedFlow<Unit>()
|
||||
val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow())
|
||||
|
||||
private val reloadManga = MutableSharedFlow<Unit>()
|
||||
private val reloadChapters = MutableSharedFlow<Unit>()
|
||||
|
||||
val dateTimeFormatter = uiPreferences.dateFormat().changes()
|
||||
.map {
|
||||
dateHandler.getDateFormat(it)
|
||||
reloadChapters.onStart { emit(Unit) }.flatMapLatest {
|
||||
loadingChapters.value = true
|
||||
getChapters.asFlow(params.mangaId)
|
||||
}
|
||||
.onEach {
|
||||
updateChapters(it)
|
||||
loadingChapters.value = false
|
||||
}
|
||||
.asStateFlow(dateHandler.getDateFormat(uiPreferences.dateFormat().get()))
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error when getting chapters" }
|
||||
loadingChapters.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
init {
|
||||
DownloadService.registerWatch(params.mangaId)
|
||||
.mapLatest { downloadingChapters ->
|
||||
chapters.value.forEach { chapter ->
|
||||
chapter.updateFrom(downloadingChapters)
|
||||
scope.launch {
|
||||
val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
if (mangaCategories != null) {
|
||||
_mangaCategories.value = mangaCategories.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val manga = manga.first { it != null }!!
|
||||
if (!manga.initialized) {
|
||||
refreshManga()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadManga() {
|
||||
scope.launch {
|
||||
reloadManga.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChapters() {
|
||||
scope.launch {
|
||||
reloadChapters.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapters(chapters: List<Chapter>) {
|
||||
_chapters.value = chapters.sortedByDescending { it.index }.toDownloadChapters()
|
||||
}
|
||||
|
||||
fun refreshManga() {
|
||||
scope.launch {
|
||||
refreshingManga.value = true
|
||||
val manga = refreshManga.await(
|
||||
params.mangaId,
|
||||
onError = {
|
||||
log.warn(it) { "Error when refreshing manga" }
|
||||
toast(it.message.orEmpty())
|
||||
},
|
||||
)
|
||||
if (manga != null) {
|
||||
_manga.value = manga
|
||||
}
|
||||
refreshingManga.value = false
|
||||
}
|
||||
scope.launch {
|
||||
refreshingChapters.value = true
|
||||
val chapters = refreshChapters.await(
|
||||
params.mangaId,
|
||||
onError = {
|
||||
log.warn(it) { "Error when refreshing chapters" }
|
||||
toast(it.message.orEmpty())
|
||||
},
|
||||
)
|
||||
if (!chapters.isNullOrEmpty()) {
|
||||
updateChapters(chapters)
|
||||
}
|
||||
refreshingChapters.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun setCategories() {
|
||||
scope.launch {
|
||||
manga.value ?: return@launch
|
||||
chooseCategoriesFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFavorite() {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
if (manga.inLibrary) {
|
||||
removeMangaFromLibrary.await(manga, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
if (categories.value.isEmpty()) {
|
||||
addFavorite(emptyList(), emptyList())
|
||||
} else {
|
||||
chooseCategoriesFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
reloadManga.onStart { emit(Unit) }.flatMapLatest {
|
||||
loadingManga.value = true
|
||||
getManga.asFlow(params.mangaId)
|
||||
loadManga()
|
||||
}
|
||||
.onEach {
|
||||
_manga.value = it
|
||||
loadingManga.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error when loading manga" }
|
||||
loadingManga.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
reloadChapters.onStart { emit(Unit) }.flatMapLatest {
|
||||
loadingChapters.value = true
|
||||
getChapters.asFlow(params.mangaId)
|
||||
}
|
||||
.onEach {
|
||||
updateChapters(it)
|
||||
loadingChapters.value = false
|
||||
fun addFavorite(
|
||||
categories: List<Category>,
|
||||
oldCategories: List<Category>,
|
||||
) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
if (manga.inLibrary) {
|
||||
if (oldCategories.isEmpty()) {
|
||||
removeMangaFromCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
oldCategories.filterNot { it in categories }.forEach {
|
||||
removeMangaFromCategory.await(manga, it, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addMangaToLibrary.await(manga, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error when getting chapters" }
|
||||
loadingChapters.value = false
|
||||
if (categories.isEmpty()) {
|
||||
addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
categories.filterNot { it in oldCategories }.forEach {
|
||||
addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
scope.launch {
|
||||
val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) })
|
||||
if (mangaCategories != null) {
|
||||
_mangaCategories.value = mangaCategories.toImmutableList()
|
||||
}
|
||||
|
||||
loadManga()
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val manga = manga.first { it != null }!!
|
||||
if (!manga.initialized) {
|
||||
refreshManga()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadManga() {
|
||||
scope.launch {
|
||||
reloadManga.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChapters() {
|
||||
scope.launch {
|
||||
reloadChapters.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChapters(chapters: List<Chapter>) {
|
||||
_chapters.value = chapters.sortedByDescending { it.index }.toDownloadChapters()
|
||||
}
|
||||
|
||||
fun refreshManga() {
|
||||
scope.launch {
|
||||
refreshingManga.value = true
|
||||
val manga = refreshManga.await(
|
||||
params.mangaId,
|
||||
onError = {
|
||||
log.warn(it) { "Error when refreshing manga" }
|
||||
toast(it.message.orEmpty())
|
||||
}
|
||||
)
|
||||
if (manga != null) {
|
||||
_manga.value = manga
|
||||
}
|
||||
refreshingManga.value = false
|
||||
}
|
||||
scope.launch {
|
||||
refreshingChapters.value = true
|
||||
val chapters = refreshChapters.await(
|
||||
params.mangaId,
|
||||
onError = {
|
||||
log.warn(it) { "Error when refreshing chapters" }
|
||||
toast(it.message.orEmpty())
|
||||
}
|
||||
)
|
||||
if (!chapters.isNullOrEmpty()) {
|
||||
updateChapters(chapters)
|
||||
}
|
||||
refreshingChapters.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun setCategories() {
|
||||
scope.launch {
|
||||
manga.value ?: return@launch
|
||||
chooseCategoriesFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFavorite() {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
if (manga.inLibrary) {
|
||||
removeMangaFromLibrary.await(manga, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
if (categories.value.isEmpty()) {
|
||||
addFavorite(emptyList(), emptyList())
|
||||
} else {
|
||||
chooseCategoriesFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
loadManga()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addFavorite(
|
||||
categories: List<Category>,
|
||||
oldCategories: List<Category>,
|
||||
) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
if (manga.inLibrary) {
|
||||
if (oldCategories.isEmpty()) {
|
||||
removeMangaFromCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
oldCategories.filterNot { it in categories }.forEach {
|
||||
removeMangaFromCategory.await(manga, it, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addMangaToLibrary.await(manga, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
if (categories.isEmpty()) {
|
||||
addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
categories.filterNot { it in oldCategories }.forEach {
|
||||
addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) })
|
||||
if (mangaCategories != null) {
|
||||
_mangaCategories.value = mangaCategories.toImmutableList()
|
||||
}
|
||||
|
||||
loadManga()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRead(
|
||||
chapterIds: List<Long>,
|
||||
read: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
manga.value?.let {
|
||||
updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
loadChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
private fun setBookmarked(
|
||||
chapterIds: List<Long>,
|
||||
bookmark: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
manga.value?.let {
|
||||
updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
loadChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
fun markPreviousRead(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let {
|
||||
val chapters = chapters.value
|
||||
.sortedBy { it.chapter.index }
|
||||
.subList(0, index).map{it.chapter.id} // todo test
|
||||
updateChapter.await(chapters, read = true, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
loadChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapter(chapterId: Long) {
|
||||
scope.launch { queueChapterDownload.await(chapterId, onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun deleteDownload(id: Long?) {
|
||||
scope.launch {
|
||||
if (id == null) {
|
||||
val chapterIds = _selectedIds.value
|
||||
deleteChapterDownload.await(chapterIds, onError = { toast(it.message.orEmpty()) })
|
||||
selectedItems.value.forEach {
|
||||
it.setNotDownloaded()
|
||||
}
|
||||
_selectedIds.value = persistentListOf()
|
||||
} else {
|
||||
chapters.value.find { it.chapter.id == id }
|
||||
?.deleteDownload(deleteChapterDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDownloadingChapter(chapterId: Long) {
|
||||
scope.launch {
|
||||
chapters.value.find { it.chapter.id == chapterId }
|
||||
?.stopDownloading(stopChapterDownload)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
scope.launch {
|
||||
_selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
scope.launch {
|
||||
_selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectChapter(id: Long) {
|
||||
scope.launch {
|
||||
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectChapter(id: Long) {
|
||||
scope.launch {
|
||||
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
scope.launch {
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(_selectedIds.value)
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadNext(next: Int) {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id }
|
||||
.takeLast(next),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadUnread() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAll() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value
|
||||
.filter { it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Chapter>.toDownloadChapters() =
|
||||
map {
|
||||
ChapterDownloadItem(null, it)
|
||||
}.toImmutableList()
|
||||
|
||||
data class Params(
|
||||
val mangaId: Long,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRead(
|
||||
chapterIds: List<Long>,
|
||||
read: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
manga.value?.let {
|
||||
updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
loadChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
private fun setBookmarked(
|
||||
chapterIds: List<Long>,
|
||||
bookmark: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
manga.value?.let {
|
||||
updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
loadChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
fun markPreviousRead(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let {
|
||||
val chapters = chapters.value
|
||||
.sortedBy { it.chapter.index }
|
||||
.subList(0, index).map { it.chapter.id } // todo test
|
||||
updateChapter.await(chapters, read = true, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
loadChapters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapter(chapterId: Long) {
|
||||
scope.launch { queueChapterDownload.await(chapterId, onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
fun deleteDownload(id: Long?) {
|
||||
scope.launch {
|
||||
if (id == null) {
|
||||
val chapterIds = _selectedIds.value
|
||||
deleteChapterDownload.await(chapterIds, onError = { toast(it.message.orEmpty()) })
|
||||
selectedItems.value.forEach {
|
||||
it.setNotDownloaded()
|
||||
}
|
||||
_selectedIds.value = persistentListOf()
|
||||
} else {
|
||||
chapters.value.find { it.chapter.id == id }
|
||||
?.deleteDownload(deleteChapterDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDownloadingChapter(chapterId: Long) {
|
||||
scope.launch {
|
||||
chapters.value.find { it.chapter.id == chapterId }
|
||||
?.stopDownloading(stopChapterDownload)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
scope.launch {
|
||||
_selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
scope.launch {
|
||||
_selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectChapter(id: Long) {
|
||||
scope.launch {
|
||||
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectChapter(id: Long) {
|
||||
scope.launch {
|
||||
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
scope.launch {
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(_selectedIds.value)
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadNext(next: Int) {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id }
|
||||
.takeLast(next),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadUnread() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAll() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value
|
||||
.filter { it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Chapter>.toDownloadChapters() =
|
||||
map {
|
||||
ChapterDownloadItem(null, it)
|
||||
}.toImmutableList()
|
||||
|
||||
data class Params(
|
||||
val mangaId: Long,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -69,372 +68,371 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class ReaderMenuViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val getManga: GetManga,
|
||||
private val getChapters: GetChapters,
|
||||
private val getChapter: GetChapter,
|
||||
private val getChapterPages: GetChapterPages,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val updateMangaMeta: UpdateMangaMeta,
|
||||
private val updateChapterMeta: UpdateChapterMeta,
|
||||
private val chapterCache: ChapterCache,
|
||||
private val http: Http,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
private val _manga = MutableStateFlow<Manga?>(null)
|
||||
private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null))
|
||||
val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
val chapter = viewerChapters.map { it.currChapter }.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
@Inject
|
||||
class ReaderMenuViewModel(
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val getManga: GetManga,
|
||||
private val getChapters: GetChapters,
|
||||
private val getChapter: GetChapter,
|
||||
private val getChapterPages: GetChapterPages,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val updateMangaMeta: UpdateMangaMeta,
|
||||
private val updateChapterMeta: UpdateChapterMeta,
|
||||
private val chapterCache: ChapterCache,
|
||||
private val http: Http,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
private val _manga = MutableStateFlow<Manga?>(null)
|
||||
private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null))
|
||||
val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
val chapter = viewerChapters.map { it.currChapter }.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val _state = MutableStateFlow<ReaderChapter.State>(ReaderChapter.State.Wait)
|
||||
val state = _state.asStateFlow()
|
||||
private val _state = MutableStateFlow<ReaderChapter.State>(ReaderChapter.State.Wait)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
val pages = viewerChapters.flatMapLatest { viewerChapters ->
|
||||
val previousChapterPages = viewerChapters.prevChapter
|
||||
?.pages
|
||||
?.map { (it as? PagesState.Success)?.pages }
|
||||
?: flowOf(null)
|
||||
val chapterPages = viewerChapters.currChapter
|
||||
?.pages
|
||||
?.map { (it as? PagesState.Success)?.pages }
|
||||
?: flowOf(null)
|
||||
val nextChapterPages = viewerChapters.nextChapter
|
||||
?.pages
|
||||
?.map { (it as? PagesState.Success)?.pages }
|
||||
?: flowOf(null)
|
||||
combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next ->
|
||||
(
|
||||
prev.orEmpty() +
|
||||
ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) +
|
||||
cur.orEmpty() +
|
||||
ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) +
|
||||
next.orEmpty()
|
||||
val pages = viewerChapters.flatMapLatest { viewerChapters ->
|
||||
val previousChapterPages = viewerChapters.prevChapter
|
||||
?.pages
|
||||
?.map { (it as? PagesState.Success)?.pages }
|
||||
?: flowOf(null)
|
||||
val chapterPages = viewerChapters.currChapter
|
||||
?.pages
|
||||
?.map { (it as? PagesState.Success)?.pages }
|
||||
?: flowOf(null)
|
||||
val nextChapterPages = viewerChapters.nextChapter
|
||||
?.pages
|
||||
?.map { (it as? PagesState.Success)?.pages }
|
||||
?: flowOf(null)
|
||||
combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next ->
|
||||
(
|
||||
prev.orEmpty() +
|
||||
ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) +
|
||||
cur.orEmpty() +
|
||||
ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) +
|
||||
next.orEmpty()
|
||||
).toImmutableList()
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _currentPage = MutableStateFlow<ReaderItem?>(null)
|
||||
val currentPage = _currentPage.asStateFlow()
|
||||
|
||||
private val _currentPageOffset = MutableStateFlow(1)
|
||||
val currentPageOffset = _currentPageOffset.asStateFlow()
|
||||
|
||||
private val _readerSettingsMenuOpen = MutableStateFlow(false)
|
||||
val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow()
|
||||
|
||||
private val _pageEmitter = MutableSharedFlow<PageMove>()
|
||||
val pageEmitter = StableHolder(_pageEmitter.asSharedFlow())
|
||||
|
||||
val readerModes = readerPreferences.modes()
|
||||
.getAsFlow()
|
||||
.map { it.toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga ->
|
||||
val mangaMode = manga?.meta?.juiReaderMode?.decodeURLQueryComponent()
|
||||
if (
|
||||
mangaMode != null &&
|
||||
mangaMode != MangaMeta.DEFAULT_READER_MODE &&
|
||||
mangaMode in readerModes.value
|
||||
) {
|
||||
mangaMode
|
||||
} else {
|
||||
mode
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get())
|
||||
|
||||
val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode)
|
||||
|
||||
private val loader = ChapterLoader(
|
||||
readerPreferences = readerPreferences,
|
||||
http = http,
|
||||
chapterCache = chapterCache,
|
||||
bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper),
|
||||
getChapterPages = getChapterPages,
|
||||
)
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
fun init() {
|
||||
scope.launchDefault {
|
||||
runCatching {
|
||||
initManga(params.mangaId)
|
||||
initChapters(params.mangaId, params.chapterId)
|
||||
}
|
||||
}
|
||||
private val _currentPage = MutableStateFlow<ReaderItem?>(null)
|
||||
val currentPage = _currentPage.asStateFlow()
|
||||
|
||||
private val _currentPageOffset = MutableStateFlow(1)
|
||||
val currentPageOffset = _currentPageOffset.asStateFlow()
|
||||
|
||||
private val _readerSettingsMenuOpen = MutableStateFlow(false)
|
||||
val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow()
|
||||
|
||||
private val _pageEmitter = MutableSharedFlow<PageMove>()
|
||||
val pageEmitter = StableHolder(_pageEmitter.asSharedFlow())
|
||||
|
||||
val readerModes = readerPreferences.modes()
|
||||
.getAsFlow()
|
||||
.map { it.toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga ->
|
||||
val mangaMode = manga?.meta?.juiReaderMode?.decodeURLQueryComponent()
|
||||
if (
|
||||
mangaMode != null &&
|
||||
mangaMode != MangaMeta.DEFAULT_READER_MODE &&
|
||||
mangaMode in readerModes.value
|
||||
) {
|
||||
mangaMode
|
||||
} else {
|
||||
mode
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get())
|
||||
|
||||
init {
|
||||
scope.launchDefault {
|
||||
currentPage
|
||||
.filterIsInstance<ReaderPage>()
|
||||
.collectLatest { page ->
|
||||
page.chapter.pageLoader?.loadPage(page)
|
||||
if (page.chapter == chapter.value) {
|
||||
val pages = page.chapter.pages.value as? PagesState.Success
|
||||
?: return@collectLatest
|
||||
if ((page.index2 + 1) >= pages.pages.size) {
|
||||
markChapterRead(page.chapter)
|
||||
}
|
||||
val nextChapter = nextChapter.value
|
||||
if (nextChapter != null && (page.index2 + 1) >= ((pages.pages.size - 5).coerceAtLeast(1))) {
|
||||
requestPreloadChapter(nextChapter)
|
||||
}
|
||||
} else {
|
||||
val previousChapter = previousChapter.value
|
||||
val nextChapter = nextChapter.value
|
||||
if (page.chapter == previousChapter) {
|
||||
viewerChapters.value = viewerChapters.value.movePrev()
|
||||
initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false)
|
||||
} else if (page.chapter == nextChapter) {
|
||||
viewerChapters.value = viewerChapters.value.moveNext()
|
||||
initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode)
|
||||
|
||||
fun navigate(navigationRegion: Navigation): Boolean {
|
||||
scope.launch {
|
||||
val moveTo = when (navigationRegion) {
|
||||
Navigation.MENU -> {
|
||||
setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value)
|
||||
null
|
||||
}
|
||||
private val loader = ChapterLoader(
|
||||
readerPreferences = readerPreferences,
|
||||
http = http,
|
||||
chapterCache = chapterCache,
|
||||
bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper),
|
||||
getChapterPages = getChapterPages,
|
||||
)
|
||||
|
||||
Navigation.NEXT -> MoveTo.Next
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
Navigation.PREV -> MoveTo.Previous
|
||||
|
||||
Navigation.RIGHT -> when (readerModeSettings.direction.value) {
|
||||
Direction.Left -> MoveTo.Previous
|
||||
else -> MoveTo.Next
|
||||
}
|
||||
|
||||
Navigation.LEFT -> when (readerModeSettings.direction.value) {
|
||||
Direction.Left -> MoveTo.Next
|
||||
else -> MoveTo.Previous
|
||||
}
|
||||
|
||||
Navigation.DOWN -> when (readerModeSettings.direction.value) {
|
||||
Direction.Up -> MoveTo.Previous
|
||||
else -> MoveTo.Next
|
||||
}
|
||||
|
||||
Navigation.UP -> when (readerModeSettings.direction.value) {
|
||||
Direction.Up -> MoveTo.Next
|
||||
else -> MoveTo.Previous
|
||||
}
|
||||
}
|
||||
if (moveTo != null) {
|
||||
_pageEmitter.emit(PageMove.Direction(moveTo))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun navigate(page: Int) {
|
||||
log.info { "Navigate to $page" }
|
||||
scope.launch {
|
||||
_pageEmitter.emit(PageMove.Page(pages.value.getOrNull(page) ?: return@launch))
|
||||
}
|
||||
}
|
||||
|
||||
fun progress(page: ReaderItem) {
|
||||
log.info { "Progressed to $page" }
|
||||
_currentPage.value = page
|
||||
}
|
||||
|
||||
fun retry(page: ReaderPage) {
|
||||
log.info { "Retrying ${page.index2}" }
|
||||
chapter.value?.pageLoader?.retryPage(page)
|
||||
}
|
||||
|
||||
fun setMangaReaderMode(mode: String) {
|
||||
scope.launchDefault {
|
||||
_manga.value?.let {
|
||||
updateMangaMeta.await(it, mode, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
fun init() {
|
||||
scope.launchDefault {
|
||||
runCatching {
|
||||
initManga(params.mangaId)
|
||||
initChapters(params.mangaId, params.chapterId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setReaderSettingsMenuOpen(open: Boolean) {
|
||||
_readerSettingsMenuOpen.value = open
|
||||
}
|
||||
|
||||
fun prevChapter() {
|
||||
scope.launchDefault {
|
||||
val prevChapter = previousChapter.value ?: return@launchDefault
|
||||
try {
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
sendProgress()
|
||||
viewerChapters.value = viewerChapters.value.movePrev()
|
||||
initChapters(params.mangaId, prevChapter.chapter.id, fromMenuButton = true)
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error loading prev chapter" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nextChapter() {
|
||||
scope.launchDefault {
|
||||
val nextChapter = nextChapter.value ?: return@launchDefault
|
||||
try {
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
sendProgress()
|
||||
viewerChapters.value = viewerChapters.value.moveNext()
|
||||
initChapters(params.mangaId, nextChapter.chapter.id, fromMenuButton = true)
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error loading next chapter" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initManga(mangaId: Long) {
|
||||
getManga.asFlow(mangaId)
|
||||
.take(1)
|
||||
.onEach {
|
||||
_manga.value = it
|
||||
}
|
||||
.catch {
|
||||
_state.value = ReaderChapter.State.Error(it)
|
||||
log.warn(it) { "Error loading manga" }
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
|
||||
private suspend fun initChapters(
|
||||
mangaId: Long,
|
||||
chapterId: Long,
|
||||
fromMenuButton: Boolean = true,
|
||||
) {
|
||||
log.debug { "Loading chapter index $chapterId" }
|
||||
val (chapter, pages) = coroutineScope {
|
||||
val chapters = getChapters.asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
_state.value = ReaderChapter.State.Error(it)
|
||||
log.warn(it) { "Error getting chapters for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
?: return@coroutineScope null
|
||||
val chapter = chapters.find { it.id == chapterId }
|
||||
?.let { ReaderChapter(it) }
|
||||
?: return@coroutineScope null
|
||||
val pages = loader.loadChapter(chapter)
|
||||
viewerChapters.update { it.copy(currChapter = chapter) }
|
||||
|
||||
if (viewerChapters.value.nextChapter == null) {
|
||||
val nextChapter = chapters.find { it.index == chapter.chapter.index + 1 }
|
||||
if (nextChapter != null) {
|
||||
val nextReaderChapter = ReaderChapter(nextChapter)
|
||||
viewerChapters.update { it.copy(nextChapter = nextReaderChapter) }
|
||||
} else {
|
||||
viewerChapters.update { it.copy(nextChapter = null) }
|
||||
}
|
||||
}
|
||||
|
||||
if (viewerChapters.value.prevChapter == null) {
|
||||
val prevChapter = chapters.find { it.index == chapter.chapter.index - 1 }
|
||||
if (prevChapter != null) {
|
||||
val prevReaderChapter = ReaderChapter(prevChapter)
|
||||
viewerChapters.update { it.copy(prevChapter = prevReaderChapter) }
|
||||
} else {
|
||||
viewerChapters.update { it.copy(prevChapter = null) }
|
||||
}
|
||||
}
|
||||
chapter to pages
|
||||
} ?: return
|
||||
|
||||
if (fromMenuButton) {
|
||||
chapter.stateObserver
|
||||
.onEach {
|
||||
_state.value = it
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
|
||||
pages
|
||||
.filterIsInstance<PagesState.Success>()
|
||||
.onEach { (pageList) ->
|
||||
val lastPageReadOffset = chapter.chapter.meta.juiPageOffset
|
||||
if (lastPageReadOffset != 0) {
|
||||
_currentPageOffset.value = lastPageReadOffset
|
||||
}
|
||||
val lastPageRead = chapter.chapter.lastPageRead
|
||||
_currentPage.value = if (lastPageRead > 0) {
|
||||
pageList[lastPageRead.coerceAtMost(pageList.lastIndex)]
|
||||
} else {
|
||||
pageList.first()
|
||||
}.also { chapter.pageLoader?.loadPage(it) }
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPreloadChapter(chapter: ReaderChapter) {
|
||||
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
||||
return
|
||||
}
|
||||
log.debug { "Preloading ${chapter.chapter.index}" }
|
||||
loader.loadChapter(chapter)
|
||||
}
|
||||
|
||||
private fun markChapterRead(chapter: ReaderChapter) {
|
||||
scope.launch {
|
||||
updateChapter.await(chapter.chapter, read = true, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun sendProgress(
|
||||
chapter: Chapter? = this.chapter.value?.chapter,
|
||||
lastPageRead: Int = (currentPage.value as? ReaderPage)?.index2 ?: 0,
|
||||
) {
|
||||
chapter ?: return
|
||||
if (chapter.read) return
|
||||
GlobalScope.launch {
|
||||
updateChapter.await(
|
||||
chapter,
|
||||
lastPageRead = lastPageRead,
|
||||
onError = { toast(it.message.orEmpty()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLastPageReadOffset(offset: Int) {
|
||||
updateLastPageReadOffset(chapter.value?.chapter ?: return, offset)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun updateLastPageReadOffset(
|
||||
chapter: Chapter,
|
||||
offset: Int,
|
||||
) {
|
||||
GlobalScope.launch {
|
||||
updateChapterMeta.await(chapter, offset, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
viewerChapters.value.recycle()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val chapterId: Long,
|
||||
val mangaId: Long,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launchDefault {
|
||||
currentPage
|
||||
.filterIsInstance<ReaderPage>()
|
||||
.collectLatest { page ->
|
||||
page.chapter.pageLoader?.loadPage(page)
|
||||
if (page.chapter == chapter.value) {
|
||||
val pages = page.chapter.pages.value as? PagesState.Success
|
||||
?: return@collectLatest
|
||||
if ((page.index2 + 1) >= pages.pages.size) {
|
||||
markChapterRead(page.chapter)
|
||||
}
|
||||
val nextChapter = nextChapter.value
|
||||
if (nextChapter != null && (page.index2 + 1) >= ((pages.pages.size - 5).coerceAtLeast(1))) {
|
||||
requestPreloadChapter(nextChapter)
|
||||
}
|
||||
} else {
|
||||
val previousChapter = previousChapter.value
|
||||
val nextChapter = nextChapter.value
|
||||
if (page.chapter == previousChapter) {
|
||||
viewerChapters.value = viewerChapters.value.movePrev()
|
||||
initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false)
|
||||
} else if (page.chapter == nextChapter) {
|
||||
viewerChapters.value = viewerChapters.value.moveNext()
|
||||
initChapters(params.mangaId, page.chapter.chapter.id, fromMenuButton = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigate(navigationRegion: Navigation): Boolean {
|
||||
scope.launch {
|
||||
val moveTo = when (navigationRegion) {
|
||||
Navigation.MENU -> {
|
||||
setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value)
|
||||
null
|
||||
}
|
||||
|
||||
Navigation.NEXT -> MoveTo.Next
|
||||
|
||||
Navigation.PREV -> MoveTo.Previous
|
||||
|
||||
Navigation.RIGHT -> when (readerModeSettings.direction.value) {
|
||||
Direction.Left -> MoveTo.Previous
|
||||
else -> MoveTo.Next
|
||||
}
|
||||
|
||||
Navigation.LEFT -> when (readerModeSettings.direction.value) {
|
||||
Direction.Left -> MoveTo.Next
|
||||
else -> MoveTo.Previous
|
||||
}
|
||||
|
||||
Navigation.DOWN -> when (readerModeSettings.direction.value) {
|
||||
Direction.Up -> MoveTo.Previous
|
||||
else -> MoveTo.Next
|
||||
}
|
||||
|
||||
Navigation.UP -> when (readerModeSettings.direction.value) {
|
||||
Direction.Up -> MoveTo.Next
|
||||
else -> MoveTo.Previous
|
||||
}
|
||||
}
|
||||
if (moveTo != null) {
|
||||
_pageEmitter.emit(PageMove.Direction(moveTo))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun navigate(page: Int) {
|
||||
log.info { "Navigate to $page" }
|
||||
scope.launch {
|
||||
_pageEmitter.emit(PageMove.Page(pages.value.getOrNull(page) ?: return@launch))
|
||||
}
|
||||
}
|
||||
|
||||
fun progress(page: ReaderItem) {
|
||||
log.info { "Progressed to $page" }
|
||||
_currentPage.value = page
|
||||
}
|
||||
|
||||
fun retry(page: ReaderPage) {
|
||||
log.info { "Retrying ${page.index2}" }
|
||||
chapter.value?.pageLoader?.retryPage(page)
|
||||
}
|
||||
|
||||
fun setMangaReaderMode(mode: String) {
|
||||
scope.launchDefault {
|
||||
_manga.value?.let {
|
||||
updateMangaMeta.await(it, mode, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
initManga(params.mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setReaderSettingsMenuOpen(open: Boolean) {
|
||||
_readerSettingsMenuOpen.value = open
|
||||
}
|
||||
|
||||
fun prevChapter() {
|
||||
scope.launchDefault {
|
||||
val prevChapter = previousChapter.value ?: return@launchDefault
|
||||
try {
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
sendProgress()
|
||||
viewerChapters.value = viewerChapters.value.movePrev()
|
||||
initChapters(params.mangaId, prevChapter.chapter.id, fromMenuButton = true)
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error loading prev chapter" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nextChapter() {
|
||||
scope.launchDefault {
|
||||
val nextChapter = nextChapter.value ?: return@launchDefault
|
||||
try {
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
sendProgress()
|
||||
viewerChapters.value = viewerChapters.value.moveNext()
|
||||
initChapters(params.mangaId, nextChapter.chapter.id, fromMenuButton = true)
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error loading next chapter" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initManga(mangaId: Long) {
|
||||
getManga.asFlow(mangaId)
|
||||
.take(1)
|
||||
.onEach {
|
||||
_manga.value = it
|
||||
}
|
||||
.catch {
|
||||
_state.value = ReaderChapter.State.Error(it)
|
||||
log.warn(it) { "Error loading manga" }
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
|
||||
private suspend fun initChapters(
|
||||
mangaId: Long,
|
||||
chapterId: Long,
|
||||
fromMenuButton: Boolean = true,
|
||||
) {
|
||||
log.debug { "Loading chapter index $chapterId" }
|
||||
val (chapter, pages) = coroutineScope {
|
||||
val chapters = getChapters.asFlow(mangaId)
|
||||
.take(1)
|
||||
.catch {
|
||||
_state.value = ReaderChapter.State.Error(it)
|
||||
log.warn(it) { "Error getting chapters for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
?: return@coroutineScope null
|
||||
val chapter = chapters.find { it.id == chapterId }
|
||||
?.let { ReaderChapter(it) }
|
||||
?: return@coroutineScope null
|
||||
val pages = loader.loadChapter(chapter)
|
||||
viewerChapters.update { it.copy(currChapter = chapter) }
|
||||
|
||||
if (viewerChapters.value.nextChapter == null) {
|
||||
val nextChapter = chapters.find { it.index == chapter.chapter.index + 1 }
|
||||
if (nextChapter != null) {
|
||||
val nextReaderChapter = ReaderChapter(nextChapter)
|
||||
viewerChapters.update { it.copy(nextChapter = nextReaderChapter) }
|
||||
} else {
|
||||
viewerChapters.update { it.copy(nextChapter = null) }
|
||||
}
|
||||
}
|
||||
|
||||
if (viewerChapters.value.prevChapter == null) {
|
||||
val prevChapter = chapters.find { it.index == chapter.chapter.index - 1 }
|
||||
if (prevChapter != null) {
|
||||
val prevReaderChapter = ReaderChapter(prevChapter)
|
||||
viewerChapters.update { it.copy(prevChapter = prevReaderChapter) }
|
||||
} else {
|
||||
viewerChapters.update { it.copy(prevChapter = null) }
|
||||
}
|
||||
}
|
||||
chapter to pages
|
||||
} ?: return
|
||||
|
||||
if (fromMenuButton) {
|
||||
chapter.stateObserver
|
||||
.onEach {
|
||||
_state.value = it
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
|
||||
pages
|
||||
.filterIsInstance<PagesState.Success>()
|
||||
.onEach { (pageList) ->
|
||||
val lastPageReadOffset = chapter.chapter.meta.juiPageOffset
|
||||
if (lastPageReadOffset != 0) {
|
||||
_currentPageOffset.value = lastPageReadOffset
|
||||
}
|
||||
val lastPageRead = chapter.chapter.lastPageRead
|
||||
_currentPage.value = if (lastPageRead > 0) {
|
||||
pageList[lastPageRead.coerceAtMost(pageList.lastIndex)]
|
||||
} else {
|
||||
pageList.first()
|
||||
}.also { chapter.pageLoader?.loadPage(it) }
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestPreloadChapter(chapter: ReaderChapter) {
|
||||
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
|
||||
return
|
||||
}
|
||||
log.debug { "Preloading ${chapter.chapter.index}" }
|
||||
loader.loadChapter(chapter)
|
||||
}
|
||||
|
||||
private fun markChapterRead(chapter: ReaderChapter) {
|
||||
scope.launch {
|
||||
updateChapter.await(chapter.chapter, read = true, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun sendProgress(
|
||||
chapter: Chapter? = this.chapter.value?.chapter,
|
||||
lastPageRead: Int = (currentPage.value as? ReaderPage)?.index2 ?: 0,
|
||||
) {
|
||||
chapter ?: return
|
||||
if (chapter.read) return
|
||||
GlobalScope.launch {
|
||||
updateChapter.await(
|
||||
chapter,
|
||||
lastPageRead = lastPageRead,
|
||||
onError = { toast(it.message.orEmpty()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLastPageReadOffset(offset: Int) {
|
||||
updateLastPageReadOffset(chapter.value?.chapter ?: return, offset)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun updateLastPageReadOffset(
|
||||
chapter: Chapter,
|
||||
offset: Int,
|
||||
) {
|
||||
GlobalScope.launch {
|
||||
updateChapterMeta.await(chapter, offset, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
viewerChapters.value.recycle()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val chapterId: Long,
|
||||
val mangaId: Long,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,47 +73,46 @@ class SettingsAdvancedScreen : Screen {
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsAdvancedViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
updatePreferences: UpdatePreferences,
|
||||
private val imageCache: ImageCache,
|
||||
private val chapterCache: ChapterCache,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val updatesEnabled = updatePreferences.enabled().asStateFlow()
|
||||
@Inject
|
||||
class SettingsAdvancedViewModel(
|
||||
updatePreferences: UpdatePreferences,
|
||||
private val imageCache: ImageCache,
|
||||
private val chapterCache: ChapterCache,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val updatesEnabled = updatePreferences.enabled().asStateFlow()
|
||||
|
||||
val imageCacheSize = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
emit(imageCache.size.bytesIntoHumanReadable())
|
||||
delay(1.seconds)
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
val chapterCacheSize = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
emit(chapterCache.size.bytesIntoHumanReadable())
|
||||
delay(1.seconds)
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
fun clearImageCache() {
|
||||
scope.launchIO {
|
||||
imageCache.clear()
|
||||
}
|
||||
val imageCacheSize = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
emit(imageCache.size.bytesIntoHumanReadable())
|
||||
delay(1.seconds)
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
fun clearChapterCache() {
|
||||
scope.launchIO {
|
||||
chapterCache.clear()
|
||||
}
|
||||
val chapterCacheSize = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
emit(chapterCache.size.bytesIntoHumanReadable())
|
||||
delay(1.seconds)
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
fun clearImageCache() {
|
||||
scope.launchIO {
|
||||
imageCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChapterCache() {
|
||||
scope.launchIO {
|
||||
chapterCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsAdvancedScreenContent(
|
||||
updatesEnabled: PreferenceMutableStateFlow<Boolean>,
|
||||
|
||||
@@ -108,23 +108,22 @@ class SettingsAppearanceScreen : Screen {
|
||||
}
|
||||
}
|
||||
|
||||
class ThemesViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val themeMode = uiPreferences.themeMode().asStateFlow()
|
||||
val lightTheme = uiPreferences.lightTheme().asStateFlow()
|
||||
val darkTheme = uiPreferences.darkTheme().asStateFlow()
|
||||
val lightColors = uiPreferences.getLightColors().asStateFlow(scope)
|
||||
val darkColors = uiPreferences.getDarkColors().asStateFlow(scope)
|
||||
@Inject
|
||||
class ThemesViewModel(
|
||||
private val uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val themeMode = uiPreferences.themeMode().asStateFlow()
|
||||
val lightTheme = uiPreferences.lightTheme().asStateFlow()
|
||||
val darkTheme = uiPreferences.darkTheme().asStateFlow()
|
||||
val lightColors = uiPreferences.getLightColors().asStateFlow(scope)
|
||||
val darkColors = uiPreferences.getDarkColors().asStateFlow(scope)
|
||||
|
||||
val windowDecorations = uiPreferences.windowDecorations().asStateFlow()
|
||||
val windowDecorations = uiPreferences.windowDecorations().asStateFlow()
|
||||
|
||||
@Composable
|
||||
fun getActiveColors(): AppColorsPreferenceState = if (MaterialTheme.colors.isLight) lightColors else darkColors
|
||||
}
|
||||
@Composable
|
||||
fun getActiveColors(): AppColorsPreferenceState = if (MaterialTheme.colors.isLight) lightColors else darkColors
|
||||
}
|
||||
|
||||
expect val showWindowDecorationsOption: Boolean
|
||||
|
||||
|
||||
@@ -118,157 +118,156 @@ class SettingsBackupScreen : Screen {
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsBackupViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val validateBackupFile: ValidateBackupFile,
|
||||
private val importBackupFile: ImportBackupFile,
|
||||
private val exportBackupFile: ExportBackupFile,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _restoreStatus = MutableStateFlow<Status>(Status.Nothing)
|
||||
val restoreStatus = _restoreStatus.asStateFlow()
|
||||
@Inject
|
||||
class SettingsBackupViewModel(
|
||||
private val validateBackupFile: ValidateBackupFile,
|
||||
private val importBackupFile: ImportBackupFile,
|
||||
private val exportBackupFile: ExportBackupFile,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _restoreStatus = MutableStateFlow<Status>(Status.Nothing)
|
||||
val restoreStatus = _restoreStatus.asStateFlow()
|
||||
|
||||
private val _missingSourceFlow = MutableSharedFlow<Pair<Path, ImmutableList<String>>>()
|
||||
val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow())
|
||||
private val _missingSourceFlow = MutableSharedFlow<Pair<Path, ImmutableList<String>>>()
|
||||
val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow())
|
||||
|
||||
private val _creatingStatus = MutableStateFlow<Status>(Status.Nothing)
|
||||
val creatingStatus = _creatingStatus.asStateFlow()
|
||||
private val _createFlow = MutableSharedFlow<String>()
|
||||
val createFlowHolder = StableHolder(_createFlow.asSharedFlow())
|
||||
private val _creatingStatus = MutableStateFlow<Status>(Status.Nothing)
|
||||
val creatingStatus = _creatingStatus.asStateFlow()
|
||||
private val _createFlow = MutableSharedFlow<String>()
|
||||
val createFlowHolder = StableHolder(_createFlow.asSharedFlow())
|
||||
|
||||
fun restoreFile(source: Source) {
|
||||
scope.launch {
|
||||
val file = try {
|
||||
FileSystem.SYSTEM_TEMPORARY_DIRECTORY
|
||||
.resolve("tachidesk.${Random.nextLong()}.tachibk")
|
||||
.also { file ->
|
||||
source.saveTo(file)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error creating backup file" }
|
||||
_restoreStatus.value = Status.Error
|
||||
e.throwIfCancellation()
|
||||
return@launch
|
||||
}
|
||||
|
||||
validateBackupFile.asFlow(file)
|
||||
.onEach { (missingSources) ->
|
||||
if (missingSources.isEmpty()) {
|
||||
restoreBackup(file)
|
||||
} else {
|
||||
_missingSourceFlow.emit(file to missingSources.toImmutableList())
|
||||
}
|
||||
fun restoreFile(source: Source) {
|
||||
scope.launch {
|
||||
val file = try {
|
||||
FileSystem.SYSTEM_TEMPORARY_DIRECTORY
|
||||
.resolve("tachidesk.${Random.nextLong()}.tachibk")
|
||||
.also { file ->
|
||||
source.saveTo(file)
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error importing backup" }
|
||||
_restoreStatus.value = Status.Error
|
||||
}
|
||||
.collect()
|
||||
} catch (e: Exception) {
|
||||
log.warn(e) { "Error creating backup file" }
|
||||
_restoreStatus.value = Status.Error
|
||||
e.throwIfCancellation()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreBackup(file: Path) {
|
||||
importBackupFile
|
||||
.asFlow(file)
|
||||
.onStart {
|
||||
_restoreStatus.value = Status.InProgress(null)
|
||||
}
|
||||
.onEach {
|
||||
_restoreStatus.value = it.second.toStatus()
|
||||
validateBackupFile.asFlow(file)
|
||||
.onEach { (missingSources) ->
|
||||
if (missingSources.isEmpty()) {
|
||||
restoreBackup(file)
|
||||
} else {
|
||||
_missingSourceFlow.emit(file to missingSources.toImmutableList())
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error importing backup" }
|
||||
_restoreStatus.value = Status.Error
|
||||
}
|
||||
.launchIn(scope)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun RestoreStatus.toStatus() = when (state) {
|
||||
RestoreState.IDLE -> Status.Success
|
||||
RestoreState.SUCCESS -> Status.Success
|
||||
RestoreState.FAILURE -> Status.Error
|
||||
RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f)
|
||||
RestoreState.RESTORING_SETTINGS -> Status.InProgress(0.02f)
|
||||
RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 0.99f))
|
||||
RestoreState.RESTORING_META -> Status.InProgress(1f)
|
||||
RestoreState.UNKNOWN -> Status.Error
|
||||
}
|
||||
fun restoreBackup(file: Path) {
|
||||
importBackupFile
|
||||
.asFlow(file)
|
||||
.onStart {
|
||||
_restoreStatus.value = Status.InProgress(null)
|
||||
}
|
||||
.onEach {
|
||||
_restoreStatus.value = it.second.toStatus()
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error importing backup" }
|
||||
_restoreStatus.value = Status.Error
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun stopRestore() {
|
||||
_restoreStatus.value = Status.Error
|
||||
}
|
||||
private fun RestoreStatus.toStatus() = when (state) {
|
||||
RestoreState.IDLE -> Status.Success
|
||||
RestoreState.SUCCESS -> Status.Success
|
||||
RestoreState.FAILURE -> Status.Error
|
||||
RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f)
|
||||
RestoreState.RESTORING_SETTINGS -> Status.InProgress(0.02f)
|
||||
RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 0.99f))
|
||||
RestoreState.RESTORING_META -> Status.InProgress(1f)
|
||||
RestoreState.UNKNOWN -> Status.Error
|
||||
}
|
||||
|
||||
private val tempFile = MutableStateFlow<Path?>(null)
|
||||
private val mutex = Mutex()
|
||||
fun stopRestore() {
|
||||
_restoreStatus.value = Status.Error
|
||||
}
|
||||
|
||||
fun exportBackup() {
|
||||
exportBackupFile
|
||||
.asFlow(
|
||||
true, true // todo
|
||||
) {
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
_creatingStatus.value = Status.InProgress(
|
||||
(bytesSentTotal.toFloat() / contentLength)
|
||||
.coerceAtMost(0.99F),
|
||||
)
|
||||
}
|
||||
private val tempFile = MutableStateFlow<Path?>(null)
|
||||
private val mutex = Mutex()
|
||||
|
||||
fun exportBackup() {
|
||||
exportBackupFile
|
||||
.asFlow(
|
||||
true, true, // todo
|
||||
) {
|
||||
onDownload { bytesSentTotal, contentLength ->
|
||||
_creatingStatus.value = Status.InProgress(
|
||||
(bytesSentTotal.toFloat() / contentLength)
|
||||
.coerceAtMost(0.99F),
|
||||
)
|
||||
}
|
||||
.onStart {
|
||||
_creatingStatus.value = Status.InProgress(null)
|
||||
}
|
||||
.onEach { (filename, source) ->
|
||||
tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also {
|
||||
mutex.tryLock()
|
||||
scope.launch {
|
||||
try {
|
||||
source.saveTo(it)
|
||||
} catch (e: Exception) {
|
||||
e.throwIfCancellation()
|
||||
log.warn(e) { "Error creating backup" }
|
||||
_creatingStatus.value = Status.Error
|
||||
} finally {
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
_createFlow.emit(filename)
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error exporting backup" }
|
||||
_creatingStatus.value = Status.Error
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun exportBackupFileFound(backupSink: Sink) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val tempFile = tempFile.value
|
||||
if (creatingStatus.value is Status.InProgress && tempFile != null) {
|
||||
}
|
||||
.onStart {
|
||||
_creatingStatus.value = Status.InProgress(null)
|
||||
}
|
||||
.onEach { (filename, source) ->
|
||||
tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also {
|
||||
mutex.tryLock()
|
||||
scope.launch {
|
||||
try {
|
||||
FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer())
|
||||
_creatingStatus.value = Status.Success
|
||||
source.saveTo(it)
|
||||
} catch (e: Exception) {
|
||||
e.throwIfCancellation()
|
||||
log.error(e) { "Error moving created backup" }
|
||||
log.warn(e) { "Error creating backup" }
|
||||
_creatingStatus.value = Status.Error
|
||||
} finally {
|
||||
mutex.unlock()
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
_createFlow.emit(filename)
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error exporting backup" }
|
||||
_creatingStatus.value = Status.Error
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun exportBackupFileFound(backupSink: Sink) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val tempFile = tempFile.value
|
||||
if (creatingStatus.value is Status.InProgress && tempFile != null) {
|
||||
try {
|
||||
FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer())
|
||||
_creatingStatus.value = Status.Success
|
||||
} catch (e: Exception) {
|
||||
e.throwIfCancellation()
|
||||
log.error(e) { "Error moving created backup" }
|
||||
_creatingStatus.value = Status.Error
|
||||
}
|
||||
} else {
|
||||
_creatingStatus.value = Status.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Status {
|
||||
data object Nothing : Status()
|
||||
|
||||
|
||||
@@ -78,63 +78,67 @@ class SettingsGeneralScreen : Screen {
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsGeneralViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val dateHandler: DateHandler,
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val startScreen = uiPreferences.startScreen().asStateFlow()
|
||||
val confirmExit = uiPreferences.confirmExit().asStateFlow()
|
||||
val language = uiPreferences.language().asStateFlow()
|
||||
val dateFormat = uiPreferences.dateFormat().asStateFlow()
|
||||
@Inject
|
||||
class SettingsGeneralViewModel(
|
||||
private val dateHandler: DateHandler,
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val startScreen = uiPreferences.startScreen().asStateFlow()
|
||||
val confirmExit = uiPreferences.confirmExit().asStateFlow()
|
||||
val language = uiPreferences.language().asStateFlow()
|
||||
val dateFormat = uiPreferences.dateFormat().asStateFlow()
|
||||
|
||||
private val now = Clock.System.now()
|
||||
private val currentLocale = Locale.current
|
||||
private val now = Clock.System.now()
|
||||
private val currentLocale = Locale.current
|
||||
|
||||
@Composable
|
||||
fun getStartScreenChoices(): ImmutableMap<StartScreen, String> =
|
||||
persistentMapOf(
|
||||
StartScreen.Library to stringResource(MR.strings.location_library),
|
||||
StartScreen.Updates to stringResource(MR.strings.location_updates),
|
||||
StartScreen.Sources to stringResource(MR.strings.location_sources),
|
||||
StartScreen.Extensions to stringResource(MR.strings.location_extensions),
|
||||
)
|
||||
@Composable
|
||||
fun getStartScreenChoices(): ImmutableMap<StartScreen, String> =
|
||||
persistentMapOf(
|
||||
StartScreen.Library to stringResource(MR.strings.location_library),
|
||||
StartScreen.Updates to stringResource(MR.strings.location_updates),
|
||||
StartScreen.Sources to stringResource(MR.strings.location_sources),
|
||||
StartScreen.Extensions to stringResource(MR.strings.location_extensions),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun getLanguageChoices(): ImmutableMap<String, String> {
|
||||
val langJsonState = MR.files.languages_json.readTextAsync()
|
||||
val langs by produceState(emptyMap(), langJsonState.value) {
|
||||
val langJson = langJsonState.value
|
||||
if (langJson != null) {
|
||||
withIOContext {
|
||||
value = Json.decodeFromString<JsonObject>(langJson)["langs"]
|
||||
?.jsonArray
|
||||
.orEmpty()
|
||||
.map { it.jsonPrimitive.content }
|
||||
.associateWith { Locale(it).getDisplayName(currentLocale) }
|
||||
}
|
||||
@Composable
|
||||
fun getLanguageChoices(): ImmutableMap<String, String> {
|
||||
val langJsonState = MR.files.languages_json.readTextAsync()
|
||||
val langs by produceState(emptyMap(), langJsonState.value) {
|
||||
val langJson = langJsonState.value
|
||||
if (langJson != null) {
|
||||
withIOContext {
|
||||
value = Json.decodeFromString<JsonObject>(langJson)["langs"]
|
||||
?.jsonArray
|
||||
.orEmpty()
|
||||
.map { it.jsonPrimitive.content }
|
||||
.associateWith { Locale(it).getDisplayName(currentLocale) }
|
||||
}
|
||||
}
|
||||
return mapOf("" to stringResource(MR.strings.language_system_default, currentLocale.getDisplayName(currentLocale)))
|
||||
.plus(langs)
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDateChoices(): ImmutableMap<String, String> =
|
||||
dateHandler.formatOptions
|
||||
.associateWith {
|
||||
it.ifEmpty { stringResource(MR.strings.date_system_default) } +
|
||||
" (${getFormattedDate(it)})"
|
||||
}
|
||||
.toImmutableMap()
|
||||
|
||||
@Composable
|
||||
private fun getFormattedDate(prefValue: String): String = dateHandler.getDateFormat(prefValue).invoke(now)
|
||||
return mapOf(
|
||||
"" to stringResource(
|
||||
MR.strings.language_system_default,
|
||||
currentLocale.getDisplayName(currentLocale),
|
||||
),
|
||||
)
|
||||
.plus(langs)
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDateChoices(): ImmutableMap<String, String> =
|
||||
dateHandler.formatOptions
|
||||
.associateWith {
|
||||
it.ifEmpty { stringResource(MR.strings.date_system_default) } +
|
||||
" (${getFormattedDate(it)})"
|
||||
}
|
||||
.toImmutableMap()
|
||||
|
||||
@Composable
|
||||
private fun getFormattedDate(prefValue: String): String = dateHandler.getDateFormat(prefValue).invoke(now)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsGeneralScreenContent(
|
||||
startScreen: PreferenceMutableStateFlow<StartScreen>,
|
||||
|
||||
@@ -94,38 +94,37 @@ class SettingsLibraryScreen : Screen {
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsLibraryViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
private val getCategories: GetCategories,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val displayMode = libraryPreferences.displayMode().asStateFlow()
|
||||
val gridColumns = libraryPreferences.gridColumns().asStateFlow()
|
||||
val gridSize = libraryPreferences.gridSize().asStateFlow()
|
||||
@Inject
|
||||
class SettingsLibraryViewModel(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
private val getCategories: GetCategories,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val displayMode = libraryPreferences.displayMode().asStateFlow()
|
||||
val gridColumns = libraryPreferences.gridColumns().asStateFlow()
|
||||
val gridSize = libraryPreferences.gridSize().asStateFlow()
|
||||
|
||||
val showAllCategory = libraryPreferences.showAllCategory().asStateFlow()
|
||||
private val _categories = MutableStateFlow(0)
|
||||
val categories = _categories.asStateFlow()
|
||||
val showAllCategory = libraryPreferences.showAllCategory().asStateFlow()
|
||||
private val _categories = MutableStateFlow(0)
|
||||
val categories = _categories.asStateFlow()
|
||||
|
||||
init {
|
||||
refreshCategoryCount()
|
||||
}
|
||||
|
||||
fun refreshCategoryCount() {
|
||||
scope.launch {
|
||||
_categories.value = getCategories.await(true, onError = { toast(it.message.orEmpty()) })?.size ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDisplayModeChoices() =
|
||||
DisplayMode.entries
|
||||
.associateWith { stringResource(it.res) }
|
||||
.toImmutableMap()
|
||||
init {
|
||||
refreshCategoryCount()
|
||||
}
|
||||
|
||||
fun refreshCategoryCount() {
|
||||
scope.launch {
|
||||
_categories.value = getCategories.await(true, onError = { toast(it.message.orEmpty()) })?.size ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDisplayModeChoices() =
|
||||
DisplayMode.entries
|
||||
.associateWith { stringResource(it.res) }
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsLibraryScreenContent(
|
||||
displayMode: PreferenceMutableStateFlow<DisplayMode>,
|
||||
|
||||
@@ -88,75 +88,75 @@ class SettingsReaderScreen : Screen {
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsReaderViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
readerPreferences: ReaderPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val modes = readerPreferences.modes().asStateFlow()
|
||||
.map {
|
||||
it.associateWith { it }
|
||||
.toImmutableMap()
|
||||
}
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentMapOf())
|
||||
val selectedMode = readerPreferences.mode().asStateIn(scope)
|
||||
@Inject
|
||||
class SettingsReaderViewModel(
|
||||
readerPreferences: ReaderPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val modes = readerPreferences.modes().asStateFlow()
|
||||
.map {
|
||||
it.associateWith { it }
|
||||
.toImmutableMap()
|
||||
}
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentMapOf())
|
||||
val selectedMode = readerPreferences.mode().asStateIn(scope)
|
||||
|
||||
private val _modeSettings = MutableStateFlow<ImmutableList<StableHolder<ReaderModePreference>>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
val modeSettings = _modeSettings.asStateFlow()
|
||||
private val _modeSettings = MutableStateFlow<ImmutableList<StableHolder<ReaderModePreference>>>(
|
||||
persistentListOf(),
|
||||
)
|
||||
val modeSettings = _modeSettings.asStateFlow()
|
||||
|
||||
init {
|
||||
modes.onEach { modes ->
|
||||
val modeSettings = _modeSettings.value
|
||||
val modesInSettings = modeSettings.map { it.item.mode }
|
||||
_modeSettings.value = modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) ->
|
||||
init {
|
||||
modes.onEach { modes ->
|
||||
val modeSettings = _modeSettings.value
|
||||
val modesInSettings = modeSettings.map { it.item.mode }
|
||||
_modeSettings.value =
|
||||
modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) ->
|
||||
it !in modesInSettings
|
||||
}.map { (it) ->
|
||||
StableHolder(ReaderModePreference(scope, it, readerPreferences.getMode(it)))
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun getDirectionChoices() =
|
||||
Direction.entries.associateWith { it.res.toPlatformString() }
|
||||
.toImmutableMap()
|
||||
|
||||
fun getPaddingChoices() =
|
||||
mapOf(
|
||||
0 to MR.strings.page_padding_none.toPlatformString(),
|
||||
8 to "8 Dp",
|
||||
16 to "16 Dp",
|
||||
32 to "32 Dp",
|
||||
).toImmutableMap()
|
||||
|
||||
fun getMaxSizeChoices(direction: Direction) =
|
||||
if (direction == Direction.Right || direction == Direction.Left) {
|
||||
mapOf(
|
||||
0 to MR.strings.max_size_unrestricted.toPlatformString(),
|
||||
700 to "700 Dp",
|
||||
900 to "900 Dp",
|
||||
1100 to "1100 Dp",
|
||||
)
|
||||
} else {
|
||||
mapOf(
|
||||
0 to MR.strings.max_size_unrestricted.toPlatformString(),
|
||||
500 to "500 Dp",
|
||||
700 to "700 Dp",
|
||||
900 to "900 Dp",
|
||||
)
|
||||
}.toImmutableMap()
|
||||
|
||||
fun getImageScaleChoices() =
|
||||
ImageScale.entries.associateWith { it.res.toPlatformString() }
|
||||
.toImmutableMap()
|
||||
|
||||
fun getNavigationModeChoices() =
|
||||
NavigationMode.entries.associateWith { it.res.toPlatformString() }
|
||||
.toImmutableMap()
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun getDirectionChoices() =
|
||||
Direction.entries.associateWith { it.res.toPlatformString() }
|
||||
.toImmutableMap()
|
||||
|
||||
fun getPaddingChoices() =
|
||||
mapOf(
|
||||
0 to MR.strings.page_padding_none.toPlatformString(),
|
||||
8 to "8 Dp",
|
||||
16 to "16 Dp",
|
||||
32 to "32 Dp",
|
||||
).toImmutableMap()
|
||||
|
||||
fun getMaxSizeChoices(direction: Direction) =
|
||||
if (direction == Direction.Right || direction == Direction.Left) {
|
||||
mapOf(
|
||||
0 to MR.strings.max_size_unrestricted.toPlatformString(),
|
||||
700 to "700 Dp",
|
||||
900 to "900 Dp",
|
||||
1100 to "1100 Dp",
|
||||
)
|
||||
} else {
|
||||
mapOf(
|
||||
0 to MR.strings.max_size_unrestricted.toPlatformString(),
|
||||
500 to "500 Dp",
|
||||
700 to "700 Dp",
|
||||
900 to "900 Dp",
|
||||
)
|
||||
}.toImmutableMap()
|
||||
|
||||
fun getImageScaleChoices() =
|
||||
ImageScale.entries.associateWith { it.res.toPlatformString() }
|
||||
.toImmutableMap()
|
||||
|
||||
fun getNavigationModeChoices() =
|
||||
NavigationMode.entries.associateWith { it.res.toPlatformString() }
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
data class ReaderModePreference(
|
||||
val scope: CoroutineScope,
|
||||
val mode: String,
|
||||
|
||||
@@ -191,7 +191,8 @@ class ServerSettings(
|
||||
getSetting = { it.backupTime },
|
||||
getInput = { SetSettingsInput(backupTime = it) },
|
||||
)
|
||||
// val basicAuthEnabled = getServerFlow(
|
||||
|
||||
// val basicAuthEnabled = getServerFlow(
|
||||
// getSetting = { it.basicAuthEnabled },
|
||||
// getInput = { SetSettingsInput(basicAuthEnabled = it) },
|
||||
// )
|
||||
@@ -263,7 +264,8 @@ class ServerSettings(
|
||||
getSetting = { it.globalUpdateInterval.toString() },
|
||||
getInput = { SetSettingsInput(globalUpdateInterval = it.toDoubleOrNull()?.takeIf { it !in 0.01..5.99 }) },
|
||||
)
|
||||
// val gqlDebugLogsEnabled = getServerFlow(
|
||||
|
||||
// val gqlDebugLogsEnabled = getServerFlow(
|
||||
// getSetting = { it.gqlDebugLogsEnabled },
|
||||
// getInput = { SetSettingsInput(gqlDebugLogsEnabled = it) },
|
||||
// )
|
||||
@@ -360,67 +362,66 @@ class ServerSettings(
|
||||
)
|
||||
}
|
||||
|
||||
class SettingsServerViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getSettings: GetSettings,
|
||||
private val setSettings: SetSettings,
|
||||
serverPreferences: ServerPreferences,
|
||||
serverHostPreferences: ServerHostPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val serverUrl = serverPreferences.server().asStateIn(scope)
|
||||
val serverPort = serverPreferences.port().asStringStateIn(scope)
|
||||
val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(scope)
|
||||
@Inject
|
||||
class SettingsServerViewModel(
|
||||
private val getSettings: GetSettings,
|
||||
private val setSettings: SetSettings,
|
||||
serverPreferences: ServerPreferences,
|
||||
serverHostPreferences: ServerHostPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val serverUrl = serverPreferences.server().asStateIn(scope)
|
||||
val serverPort = serverPreferences.port().asStringStateIn(scope)
|
||||
val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(scope)
|
||||
|
||||
val proxy = serverPreferences.proxy().asStateIn(scope)
|
||||
val proxy = serverPreferences.proxy().asStateIn(scope)
|
||||
|
||||
val host = serverHostPreferences.host().asStateIn(scope)
|
||||
val host = serverHostPreferences.host().asStateIn(scope)
|
||||
|
||||
@Composable
|
||||
fun getProxyChoices(): ImmutableMap<Proxy, String> =
|
||||
persistentMapOf(
|
||||
Proxy.NO_PROXY to stringResource(MR.strings.no_proxy),
|
||||
Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy),
|
||||
Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy),
|
||||
)
|
||||
@Composable
|
||||
fun getProxyChoices(): ImmutableMap<Proxy, String> =
|
||||
persistentMapOf(
|
||||
Proxy.NO_PROXY to stringResource(MR.strings.no_proxy),
|
||||
Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy),
|
||||
Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy),
|
||||
)
|
||||
|
||||
val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope)
|
||||
val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope)
|
||||
val socksHost = serverPreferences.proxySocksHost().asStateIn(scope)
|
||||
val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope)
|
||||
val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope)
|
||||
val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope)
|
||||
val socksHost = serverPreferences.proxySocksHost().asStateIn(scope)
|
||||
val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope)
|
||||
|
||||
val auth = serverPreferences.auth().asStateIn(scope)
|
||||
val auth = serverPreferences.auth().asStateIn(scope)
|
||||
|
||||
@Composable
|
||||
fun getAuthChoices(): ImmutableMap<Auth, String> =
|
||||
persistentMapOf(
|
||||
Auth.NONE to stringResource(MR.strings.no_auth),
|
||||
Auth.BASIC to stringResource(MR.strings.basic_auth),
|
||||
Auth.DIGEST to stringResource(MR.strings.digest_auth),
|
||||
)
|
||||
@Composable
|
||||
fun getAuthChoices(): ImmutableMap<Auth, String> =
|
||||
persistentMapOf(
|
||||
Auth.NONE to stringResource(MR.strings.no_auth),
|
||||
Auth.BASIC to stringResource(MR.strings.basic_auth),
|
||||
Auth.DIGEST to stringResource(MR.strings.digest_auth),
|
||||
)
|
||||
|
||||
val authUsername = serverPreferences.authUsername().asStateIn(scope)
|
||||
val authPassword = serverPreferences.authPassword().asStateIn(scope)
|
||||
val authUsername = serverPreferences.authUsername().asStateIn(scope)
|
||||
val authPassword = serverPreferences.authPassword().asStateIn(scope)
|
||||
|
||||
private val _serverSettings = MutableStateFlow<ServerSettings?>(null)
|
||||
val serverSettings = _serverSettings.asStateFlow()
|
||||
private val _serverSettings = MutableStateFlow<ServerSettings?>(null)
|
||||
val serverSettings = _serverSettings.asStateFlow()
|
||||
|
||||
init {
|
||||
scope.launchIO {
|
||||
val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) })
|
||||
if (initialSettings != null) {
|
||||
_serverSettings.value = ServerSettings(
|
||||
getSettings,
|
||||
setSettings,
|
||||
scope,
|
||||
initialSettings,
|
||||
onError = { toast(it) },
|
||||
)
|
||||
}
|
||||
init {
|
||||
scope.launchIO {
|
||||
val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) })
|
||||
if (initialSettings != null) {
|
||||
_serverSettings.value = ServerSettings(
|
||||
getSettings,
|
||||
setSettings,
|
||||
scope,
|
||||
initialSettings,
|
||||
onError = { toast(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsServerScreenContent(
|
||||
@@ -886,7 +887,7 @@ private val repoRegex =
|
||||
(
|
||||
"https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" +
|
||||
"\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?"
|
||||
).toRegex()
|
||||
).toRegex()
|
||||
|
||||
@Composable
|
||||
fun ExtensionReposDialog(
|
||||
|
||||
@@ -43,129 +43,128 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class GlobalSearchViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getSourceList: GetSourceList,
|
||||
private val getSearchManga: GetSearchManga,
|
||||
catalogPreferences: CatalogPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val savedStateHandle: SavedStateHandle,
|
||||
@Assisted params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _query by savedStateHandle.getStateFlow { params.initialQuery }
|
||||
val query = _query.asStateFlow()
|
||||
@Inject
|
||||
class GlobalSearchViewModel(
|
||||
private val getSourceList: GetSourceList,
|
||||
private val getSearchManga: GetSearchManga,
|
||||
catalogPreferences: CatalogPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val savedStateHandle: SavedStateHandle,
|
||||
@Assisted params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _query by savedStateHandle.getStateFlow { params.initialQuery }
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
private val installedSources = MutableStateFlow(emptyList<Source>())
|
||||
private val installedSources = MutableStateFlow(emptyList<Source>())
|
||||
|
||||
private val languages = catalogPreferences.languages().stateIn(scope)
|
||||
val displayMode = catalogPreferences.displayMode().stateIn(scope)
|
||||
private val languages = catalogPreferences.languages().stateIn(scope)
|
||||
val displayMode = catalogPreferences.displayMode().stateIn(scope)
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
val sources = combine(installedSources, languages) { installedSources, languages ->
|
||||
installedSources.filter {
|
||||
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
|
||||
}.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val sources = combine(installedSources, languages) { installedSources, languages ->
|
||||
installedSources.filter {
|
||||
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
|
||||
}.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val search by savedStateHandle.getStateFlow { params.initialQuery }
|
||||
private val search by savedStateHandle.getStateFlow { params.initialQuery }
|
||||
|
||||
val results = SnapshotStateMap<Long, Search>()
|
||||
val results = SnapshotStateMap<Long, Search>()
|
||||
|
||||
init {
|
||||
getSources()
|
||||
readySearch()
|
||||
}
|
||||
init {
|
||||
getSources()
|
||||
readySearch()
|
||||
}
|
||||
|
||||
private fun getSources() {
|
||||
getSourceList.asFlow()
|
||||
.onEach { sources ->
|
||||
installedSources.value = sources.sortedWith(
|
||||
compareBy<Source, String>(String.CASE_INSENSITIVE_ORDER) { it.lang }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) {
|
||||
it.name
|
||||
},
|
||||
)
|
||||
_isLoading.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error getting sources" }
|
||||
_isLoading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private val semaphore = Semaphore(5)
|
||||
|
||||
private fun readySearch() {
|
||||
search
|
||||
.combine(sources) { query, sources ->
|
||||
query to sources
|
||||
}
|
||||
.mapLatest { (query, sources) ->
|
||||
results.clear()
|
||||
supervisorScope {
|
||||
sources.map { source ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
getSearchManga.asFlow(source, 1, query, null)
|
||||
.map {
|
||||
if (it.mangaList.isEmpty()) {
|
||||
Search.Failure(MR.strings.no_results_found.toPlatformString())
|
||||
} else {
|
||||
Search.Success(it.mangaList.toImmutableList())
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
log.warn(it) { "Error getting search from ${source.displayName}" }
|
||||
emit(Search.Failure(it))
|
||||
}
|
||||
.onEach {
|
||||
results[source.id] = it
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
log.warn(it) { "Error getting sources" }
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
fun startSearch(query: String) {
|
||||
search.value = query
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val initialQuery: String,
|
||||
)
|
||||
|
||||
sealed class Search {
|
||||
data object Searching : Search()
|
||||
|
||||
data class Success(
|
||||
val mangaList: ImmutableList<Manga>,
|
||||
) : Search()
|
||||
|
||||
data class Failure(
|
||||
val e: String?,
|
||||
) : Search() {
|
||||
constructor(e: Throwable) : this(e.message)
|
||||
private fun getSources() {
|
||||
getSourceList.asFlow()
|
||||
.onEach { sources ->
|
||||
installedSources.value = sources.sortedWith(
|
||||
compareBy<Source, String>(String.CASE_INSENSITIVE_ORDER) { it.lang }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) {
|
||||
it.name
|
||||
},
|
||||
)
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error getting sources" }
|
||||
_isLoading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
private val semaphore = Semaphore(5)
|
||||
|
||||
private fun readySearch() {
|
||||
search
|
||||
.combine(sources) { query, sources ->
|
||||
query to sources
|
||||
}
|
||||
.mapLatest { (query, sources) ->
|
||||
results.clear()
|
||||
supervisorScope {
|
||||
sources.map { source ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
getSearchManga.asFlow(source, 1, query, null)
|
||||
.map {
|
||||
if (it.mangaList.isEmpty()) {
|
||||
Search.Failure(MR.strings.no_results_found.toPlatformString())
|
||||
} else {
|
||||
Search.Success(it.mangaList.toImmutableList())
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
log.warn(it) { "Error getting search from ${source.displayName}" }
|
||||
emit(Search.Failure(it))
|
||||
}
|
||||
.onEach {
|
||||
results[source.id] = it
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
log.warn(it) { "Error getting sources" }
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
fun startSearch(query: String) {
|
||||
search.value = query
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val initialQuery: String,
|
||||
)
|
||||
|
||||
sealed class Search {
|
||||
data object Searching : Search()
|
||||
|
||||
data class Success(
|
||||
val mangaList: ImmutableList<Manga>,
|
||||
) : Search()
|
||||
|
||||
data class Failure(
|
||||
val e: String?,
|
||||
) : Search() {
|
||||
constructor(e: Throwable) : this(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,100 +34,99 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class SourceHomeScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getSourceList: GetSourceList,
|
||||
catalogPreferences: CatalogPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
@Inject
|
||||
class SourceHomeScreenViewModel(
|
||||
private val getSourceList: GetSourceList,
|
||||
catalogPreferences: CatalogPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private val installedSources = MutableStateFlow(emptyList<Source>())
|
||||
private val installedSources = MutableStateFlow(emptyList<Source>())
|
||||
|
||||
private val _languages = catalogPreferences.languages().asStateFlow()
|
||||
val languages = _languages.asStateFlow()
|
||||
.map { it.toImmutableSet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
|
||||
private val _languages = catalogPreferences.languages().asStateFlow()
|
||||
val languages = _languages.asStateFlow()
|
||||
.map { it.toImmutableSet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
|
||||
|
||||
val sources = combine(installedSources, languages) { installedSources, languages ->
|
||||
val all = MR.strings.all.toPlatformString()
|
||||
val other = MR.strings.other.toPlatformString()
|
||||
installedSources
|
||||
.distinctBy { it.id }
|
||||
.filter {
|
||||
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
|
||||
val sources = combine(installedSources, languages) { installedSources, languages ->
|
||||
val all = MR.strings.all.toPlatformString()
|
||||
val other = MR.strings.other.toPlatformString()
|
||||
installedSources
|
||||
.distinctBy { it.id }
|
||||
.filter {
|
||||
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
|
||||
}
|
||||
.groupBy(Source::displayLang)
|
||||
.mapValues {
|
||||
it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name))
|
||||
.map(SourceUI::SourceItem)
|
||||
}
|
||||
.mapKeys { (key) ->
|
||||
when (key) {
|
||||
"all" -> all
|
||||
"other" -> other
|
||||
else -> Locale(key).displayName
|
||||
}
|
||||
.groupBy(Source::displayLang)
|
||||
.mapValues {
|
||||
it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name))
|
||||
.map(SourceUI::SourceItem)
|
||||
}
|
||||
.mapKeys { (key) ->
|
||||
}
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareBy<Pair<String, *>> { (key) ->
|
||||
when (key) {
|
||||
"all" -> all
|
||||
"other" -> other
|
||||
else -> Locale(key).displayName
|
||||
all -> 1
|
||||
other -> 3
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareBy<Pair<String, *>> { (key) ->
|
||||
when (key) {
|
||||
all -> 1
|
||||
other -> 3
|
||||
else -> 2
|
||||
}
|
||||
}.thenBy(String.CASE_INSENSITIVE_ORDER, Pair<String, *>::first),
|
||||
)
|
||||
.flatMap { (key, value) ->
|
||||
listOf(SourceUI.Header(key)) + value
|
||||
}
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
}.thenBy(String.CASE_INSENSITIVE_ORDER, Pair<String, *>::first),
|
||||
)
|
||||
.flatMap { (key, value) ->
|
||||
listOf(SourceUI.Header(key)) + value
|
||||
}
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val sourceLanguages = installedSources.map { sources ->
|
||||
sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG)
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val sourceLanguages = installedSources.map { sources ->
|
||||
sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG)
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _query by savedStateHandle.getStateFlow { "" }
|
||||
val query = _query.asStateFlow()
|
||||
private val _query by savedStateHandle.getStateFlow { "" }
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
init {
|
||||
getSources()
|
||||
}
|
||||
|
||||
private fun getSources() {
|
||||
getSourceList.asFlow()
|
||||
.onEach {
|
||||
installedSources.value = it
|
||||
_isLoading.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error getting sources" }
|
||||
_isLoading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setEnabledLanguages(langs: Set<String>) {
|
||||
log.info { langs }
|
||||
_languages.value = langs
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
init {
|
||||
getSources()
|
||||
}
|
||||
|
||||
private fun getSources() {
|
||||
getSourceList.asFlow()
|
||||
.onEach {
|
||||
installedSources.value = it
|
||||
_isLoading.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error getting sources" }
|
||||
_isLoading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun setEnabledLanguages(langs: Set<String>) {
|
||||
log.info { langs }
|
||||
_languages.value = langs
|
||||
}
|
||||
|
||||
fun setQuery(query: String) {
|
||||
_query.value = query
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
sealed class SourceUI {
|
||||
@Stable
|
||||
|
||||
@@ -28,65 +28,64 @@ import me.tatarka.inject.annotations.Assisted
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class SourceSettingsScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getSourceSettings: GetSourceSettings,
|
||||
private val setSourceSetting: SetSourceSetting,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
@Inject
|
||||
class SourceSettingsScreenViewModel(
|
||||
private val getSourceSettings: GetSourceSettings,
|
||||
private val setSourceSetting: SetSourceSetting,
|
||||
contextWrapper: ContextWrapper,
|
||||
@Assisted private val params: Params,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
|
||||
private val _sourceSettings = MutableStateFlow<ImmutableList<SourceSettingsView<*, *>>>(persistentListOf())
|
||||
val sourceSettings = _sourceSettings.asStateFlow()
|
||||
private val _sourceSettings = MutableStateFlow<ImmutableList<SourceSettingsView<*, *>>>(persistentListOf())
|
||||
val sourceSettings = _sourceSettings.asStateFlow()
|
||||
|
||||
init {
|
||||
getSourceSettings()
|
||||
sourceSettings.mapLatest { settings ->
|
||||
supervisorScope {
|
||||
settings.forEach { setting ->
|
||||
setting.state.drop(1)
|
||||
.filterNotNull()
|
||||
.onEach {
|
||||
setSourceSetting.await(
|
||||
sourceId = params.sourceId,
|
||||
setting.props,
|
||||
onError = { toast(it.message.orEmpty()) },
|
||||
)
|
||||
getSourceSettings()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
init {
|
||||
getSourceSettings()
|
||||
sourceSettings.mapLatest { settings ->
|
||||
supervisorScope {
|
||||
settings.forEach { setting ->
|
||||
setting.state.drop(1)
|
||||
.filterNotNull()
|
||||
.onEach {
|
||||
setSourceSetting.await(
|
||||
sourceId = params.sourceId,
|
||||
setting.props,
|
||||
onError = { toast(it.message.orEmpty()) },
|
||||
)
|
||||
getSourceSettings()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun getSourceSettings() {
|
||||
getSourceSettings.asFlow(params.sourceId)
|
||||
.onEach {
|
||||
_sourceSettings.value = it.toView()
|
||||
_loading.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error setting source setting" }
|
||||
_loading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sourceId: Long,
|
||||
)
|
||||
|
||||
private fun List<SourcePreference>.toView() =
|
||||
mapIndexed { index, sourcePreference ->
|
||||
SourceSettingsView(index, sourcePreference)
|
||||
}.toImmutableList()
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
private fun getSourceSettings() {
|
||||
getSourceSettings.asFlow(params.sourceId)
|
||||
.onEach {
|
||||
_sourceSettings.value = it.toView()
|
||||
_loading.value = false
|
||||
}
|
||||
.catch {
|
||||
toast(it.message.orEmpty())
|
||||
log.warn(it) { "Error setting source setting" }
|
||||
_loading.value = false
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sourceId: Long,
|
||||
)
|
||||
|
||||
private fun List<SourcePreference>.toView() =
|
||||
mapIndexed { index, sourcePreference ->
|
||||
SourceSettingsView(index, sourcePreference)
|
||||
}.toImmutableList()
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,204 +40,203 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
class UpdatesScreenViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val queueChapterDownload: QueueChapterDownload,
|
||||
private val stopChapterDownload: StopChapterDownload,
|
||||
private val deleteChapterDownload: DeleteChapterDownload,
|
||||
private val getRecentUpdates: GetRecentUpdates,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val batchChapterDownload: BatchChapterDownload,
|
||||
private val updateLibrary: UpdateLibrary,
|
||||
private val updatesPager: UpdatesPager,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
@Inject
|
||||
class UpdatesScreenViewModel(
|
||||
private val queueChapterDownload: QueueChapterDownload,
|
||||
private val stopChapterDownload: StopChapterDownload,
|
||||
private val deleteChapterDownload: DeleteChapterDownload,
|
||||
private val getRecentUpdates: GetRecentUpdates,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val batchChapterDownload: BatchChapterDownload,
|
||||
private val updateLibrary: UpdateLibrary,
|
||||
private val updatesPager: UpdatesPager,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
val updates = updatesPager.updates.map { updates ->
|
||||
updates.map {
|
||||
when (it) {
|
||||
is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date)
|
||||
is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter))
|
||||
val updates = updatesPager.updates.map { updates ->
|
||||
updates.map {
|
||||
when (it) {
|
||||
is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date)
|
||||
is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||
val selectedItems = combine(updates, _selectedIds) { updates, selectedItems ->
|
||||
updates.asSequence()
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.filter { it.chapterDownloadItem.isSelected(selectedItems) }
|
||||
.map { it.chapterDownloadItem }
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val inActionMode = _selectedIds.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
init {
|
||||
updatesPager.loadNextPage(
|
||||
onComplete = {
|
||||
_isLoading.value = false
|
||||
},
|
||||
onError = {
|
||||
toast(it.message.orEmpty())
|
||||
},
|
||||
)
|
||||
updates
|
||||
.map { updates ->
|
||||
updates.filterIsInstance<UpdatesUI.Item>().mapNotNull {
|
||||
it.chapterDownloadItem.manga?.id
|
||||
}.toSet()
|
||||
}
|
||||
.combine(DownloadService.downloadQueue) { mangaIds, queue ->
|
||||
mangaIds to queue
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.onEach { (mangaIds, queue) ->
|
||||
val chapters = queue.filter { it.mangaId in mangaIds }
|
||||
updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
||||
it.chapterDownloadItem.updateFrom(chapters)
|
||||
}
|
||||
}.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||
val selectedItems = combine(updates, _selectedIds) { updates, selectedItems ->
|
||||
updates.asSequence()
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.filter { it.chapterDownloadItem.isSelected(selectedItems) }
|
||||
.map { it.chapterDownloadItem }
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val inActionMode = _selectedIds.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
init {
|
||||
updatesPager.loadNextPage(
|
||||
onComplete = {
|
||||
_isLoading.value = false
|
||||
},
|
||||
onError = {
|
||||
toast(it.message.orEmpty())
|
||||
},
|
||||
)
|
||||
updates
|
||||
.map { updates ->
|
||||
updates.filterIsInstance<UpdatesUI.Item>().mapNotNull {
|
||||
it.chapterDownloadItem.manga?.id
|
||||
}.toSet()
|
||||
}
|
||||
.combine(DownloadService.downloadQueue) { mangaIds, queue ->
|
||||
mangaIds to queue
|
||||
}
|
||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
.onEach { (mangaIds, queue) ->
|
||||
val chapters = queue.filter { it.mangaId in mangaIds }
|
||||
updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
||||
it.chapterDownloadItem.updateFrom(chapters)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun loadNextPage() {
|
||||
updatesPager.loadNextPage(
|
||||
onError = {
|
||||
toast(it.message.orEmpty())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun setRead(
|
||||
chapterIds: List<Long>,
|
||||
read: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
fun loadNextPage() {
|
||||
updatesPager.loadNextPage(
|
||||
onError = {
|
||||
toast(it.message.orEmpty())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
private fun setBookmarked(
|
||||
chapterIds: List<Long>,
|
||||
bookmark: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
fun downloadChapter(chapter: Chapter?) {
|
||||
scope.launch {
|
||||
if (chapter == null) {
|
||||
val selectedIds = _selectedIds.value
|
||||
batchChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
return@launch
|
||||
}
|
||||
queueChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDownloadedChapter(chapter: Chapter?) {
|
||||
scope.launchDefault {
|
||||
if (chapter == null) {
|
||||
val selectedIds = _selectedIds.value
|
||||
deleteChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) })
|
||||
selectedItems.value.forEach {
|
||||
it.setNotDownloaded()
|
||||
}
|
||||
_selectedIds.value = persistentListOf()
|
||||
return@launchDefault
|
||||
}
|
||||
updates.value
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.find { (chapterDownloadItem) ->
|
||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||
chapterDownloadItem.chapter.index == chapter.index
|
||||
}
|
||||
?.chapterDownloadItem
|
||||
?.deleteDownload(deleteChapterDownload)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDownloadingChapter(chapter: Chapter) {
|
||||
scope.launchDefault {
|
||||
updates.value
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.find { (chapterDownloadItem) ->
|
||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||
chapterDownloadItem.chapter.index == chapter.index
|
||||
}
|
||||
?.chapterDownloadItem
|
||||
?.stopDownloading(stopChapterDownload)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||
.map { it.chapterDownloadItem.chapter.id }
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||
.map { it.chapterDownloadItem.chapter.id }
|
||||
.minus(_selectedIds.value)
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectChapter(id: Long) {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectChapter(id: Long) {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibrary() {
|
||||
scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
updatesPager.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
private fun setRead(
|
||||
chapterIds: List<Long>,
|
||||
read: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
private fun setBookmarked(
|
||||
chapterIds: List<Long>,
|
||||
bookmark: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
updateChapter.await(chapterIds, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true)
|
||||
|
||||
fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false)
|
||||
|
||||
fun downloadChapter(chapter: Chapter?) {
|
||||
scope.launch {
|
||||
if (chapter == null) {
|
||||
val selectedIds = _selectedIds.value
|
||||
batchChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) })
|
||||
_selectedIds.value = persistentListOf()
|
||||
return@launch
|
||||
}
|
||||
queueChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDownloadedChapter(chapter: Chapter?) {
|
||||
scope.launchDefault {
|
||||
if (chapter == null) {
|
||||
val selectedIds = _selectedIds.value
|
||||
deleteChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) })
|
||||
selectedItems.value.forEach {
|
||||
it.setNotDownloaded()
|
||||
}
|
||||
_selectedIds.value = persistentListOf()
|
||||
return@launchDefault
|
||||
}
|
||||
updates.value
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.find { (chapterDownloadItem) ->
|
||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||
chapterDownloadItem.chapter.index == chapter.index
|
||||
}
|
||||
?.chapterDownloadItem
|
||||
?.deleteDownload(deleteChapterDownload)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopDownloadingChapter(chapter: Chapter) {
|
||||
scope.launchDefault {
|
||||
updates.value
|
||||
.filterIsInstance<UpdatesUI.Item>()
|
||||
.find { (chapterDownloadItem) ->
|
||||
chapterDownloadItem.chapter.mangaId == chapter.mangaId &&
|
||||
chapterDownloadItem.chapter.index == chapter.index
|
||||
}
|
||||
?.chapterDownloadItem
|
||||
?.stopDownloading(stopChapterDownload)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||
.map { it.chapterDownloadItem.chapter.id }
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = updates.value.filterIsInstance<UpdatesUI.Item>()
|
||||
.map { it.chapterDownloadItem.chapter.id }
|
||||
.minus(_selectedIds.value)
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectChapter(id: Long) {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = _selectedIds.value.plus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectChapter(id: Long) {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = _selectedIds.value.minus(id).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
scope.launchDefault {
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibrary() {
|
||||
scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) }
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
updatesPager.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UpdatesUI {
|
||||
data class Item(
|
||||
val chapterDownloadItem: ChapterDownloadItem,
|
||||
|
||||
@@ -18,26 +18,26 @@ import kotlinx.coroutines.flow.shareIn
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
@Inject
|
||||
class TrayViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
updateChecker: UpdateChecker,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
constructor(
|
||||
updateChecker: UpdateChecker,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
|
||||
val updateFound = updateChecker
|
||||
.asFlow(false)
|
||||
.catch { log.warn(it) { "Failed to check for updates" } }
|
||||
.filterIsInstance<UpdateChecker.Update.UpdateFound>()
|
||||
.shareIn(scope, SharingStarted.Eagerly, 1)
|
||||
val updateFound = updateChecker
|
||||
.asFlow(false)
|
||||
.catch { log.warn(it) { "Failed to check for updates" } }
|
||||
.filterIsInstance<UpdateChecker.Update.UpdateFound>()
|
||||
.shareIn(scope, SharingStarted.Eagerly, 1)
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = logging()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,72 +66,72 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
actual class SettingsServerHostViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
serverPreferences: ServerPreferences,
|
||||
serverHostPreferences: ServerHostPreferences,
|
||||
private val serverService: ServerService,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val host = serverHostPreferences.host().asStateIn(scope)
|
||||
constructor(
|
||||
serverPreferences: ServerPreferences,
|
||||
serverHostPreferences: ServerHostPreferences,
|
||||
private val serverService: ServerService,
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
val host = serverHostPreferences.host().asStateIn(scope)
|
||||
|
||||
// IP
|
||||
val ip = serverHostPreferences.ip().asStateIn(scope)
|
||||
val port = serverHostPreferences.port().asStringStateIn(scope)
|
||||
// IP
|
||||
val ip = serverHostPreferences.ip().asStateIn(scope)
|
||||
val port = serverHostPreferences.port().asStringStateIn(scope)
|
||||
|
||||
// Root
|
||||
val rootPath = serverHostPreferences.rootPath().asStateIn(scope)
|
||||
// Root
|
||||
val rootPath = serverHostPreferences.rootPath().asStateIn(scope)
|
||||
|
||||
// Downloader
|
||||
val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope)
|
||||
// Downloader
|
||||
val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope)
|
||||
|
||||
// Backup
|
||||
val backupPath = serverHostPreferences.backupPath().asStateIn(scope)
|
||||
// Backup
|
||||
val backupPath = serverHostPreferences.backupPath().asStateIn(scope)
|
||||
|
||||
// LocalSource
|
||||
val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope)
|
||||
// LocalSource
|
||||
val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope)
|
||||
|
||||
// Authentication
|
||||
val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope)
|
||||
val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope)
|
||||
val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope)
|
||||
// Authentication
|
||||
val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope)
|
||||
val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope)
|
||||
val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope)
|
||||
|
||||
private val _serverSettingChanged = MutableStateFlow(false)
|
||||
val serverSettingChanged = _serverSettingChanged.asStateFlow()
|
||||
private val _serverSettingChanged = MutableStateFlow(false)
|
||||
val serverSettingChanged = _serverSettingChanged.asStateFlow()
|
||||
|
||||
fun serverSettingChanged() {
|
||||
_serverSettingChanged.value = true
|
||||
}
|
||||
fun serverSettingChanged() {
|
||||
_serverSettingChanged.value = true
|
||||
}
|
||||
|
||||
fun restartServer() {
|
||||
if (serverSettingChanged.value) {
|
||||
serverService.startServer()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle password connection to hosted server
|
||||
val auth = serverPreferences.auth().asStateIn(scope)
|
||||
val authUsername = serverPreferences.authUsername().asStateIn(scope)
|
||||
val authPassword = serverPreferences.authPassword().asStateIn(scope)
|
||||
|
||||
init {
|
||||
combine(host, basicAuthEnabled, basicAuthUsername, basicAuthPassword) { host, enabled, username, password ->
|
||||
if (host) {
|
||||
if (enabled) {
|
||||
auth.value = Auth.BASIC
|
||||
authUsername.value = username
|
||||
authPassword.value = password
|
||||
} else {
|
||||
auth.value = Auth.NONE
|
||||
authUsername.value = ""
|
||||
authPassword.value = ""
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
fun restartServer() {
|
||||
if (serverSettingChanged.value) {
|
||||
serverService.startServer()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle password connection to hosted server
|
||||
val auth = serverPreferences.auth().asStateIn(scope)
|
||||
val authUsername = serverPreferences.authUsername().asStateIn(scope)
|
||||
val authPassword = serverPreferences.authPassword().asStateIn(scope)
|
||||
|
||||
init {
|
||||
combine(host, basicAuthEnabled, basicAuthUsername, basicAuthPassword) { host, enabled, username, password ->
|
||||
if (host) {
|
||||
if (enabled) {
|
||||
auth.value = Auth.BASIC
|
||||
authUsername.value = username
|
||||
authPassword.value = password
|
||||
} else {
|
||||
auth.value = Auth.NONE
|
||||
authUsername.value = ""
|
||||
authPassword.value = ""
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.ServerHostItems(
|
||||
hostValue: Boolean,
|
||||
basicAuthEnabledValue: Boolean,
|
||||
|
||||
@@ -11,12 +11,12 @@ import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
@Inject
|
||||
actual class DebugOverlayViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
actual val maxMemory: String
|
||||
get() = ""
|
||||
actual val usedMemoryFlow: MutableStateFlow<String> = MutableStateFlow("")
|
||||
}
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
actual val maxMemory: String
|
||||
get() = ""
|
||||
actual val usedMemoryFlow: MutableStateFlow<String> = MutableStateFlow("")
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import me.tatarka.inject.annotations.Inject
|
||||
@Composable
|
||||
actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {}
|
||||
|
||||
@Inject
|
||||
actual class SettingsServerHostViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper)
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper)
|
||||
|
||||
@@ -16,36 +16,36 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Inject
|
||||
actual class DebugOverlayViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
) : ViewModel(contextWrapper) {
|
||||
override val scope = MainScope()
|
||||
|
||||
val runtime: Runtime = Runtime.getRuntime()
|
||||
actual val maxMemory = runtime.maxMemory().formatSize()
|
||||
actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize())
|
||||
val runtime: Runtime = Runtime.getRuntime()
|
||||
actual val maxMemory = runtime.maxMemory().formatSize()
|
||||
actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize())
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
while (true) {
|
||||
usedMemoryFlow.value = runtime.usedMemory().formatSize()
|
||||
delay(100.milliseconds)
|
||||
}
|
||||
init {
|
||||
scope.launch {
|
||||
while (true) {
|
||||
usedMemoryFlow.value = runtime.usedMemory().formatSize()
|
||||
delay(100.milliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.formatSize(): String {
|
||||
if (this < 1024) return "$this B"
|
||||
val z = (63 - java.lang.Long.numberOfLeadingZeros(this)) / 10
|
||||
return String.format("%.1f %sB", toDouble() / (1L shl z * 10), " KMGTPE"[z])
|
||||
}
|
||||
|
||||
private fun Runtime.usedMemory(): Long = totalMemory() - freeMemory()
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.formatSize(): String {
|
||||
if (this < 1024) return "$this B"
|
||||
val z = (63 - java.lang.Long.numberOfLeadingZeros(this)) / 10
|
||||
return String.format("%.1f %sB", toDouble() / (1L shl z * 10), " KMGTPE"[z])
|
||||
}
|
||||
|
||||
private fun Runtime.usedMemory(): Long = totalMemory() - freeMemory()
|
||||
|
||||
override fun onDispose() {
|
||||
super.onDispose()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user