Fix formatting in a bunch of files

This commit is contained in:
Syer10
2025-10-03 21:20:55 -04:00
parent a7cc5e664b
commit bdd4ef20cb
92 changed files with 4647 additions and 4709 deletions

View File

@@ -23,103 +23,102 @@ import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ServerListeners @Inject
@Inject class ServerListeners {
constructor() { val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
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>>( private val _mangaListener = MutableSharedFlow<List<Long>>(
extraBufferCapacity = Channel.UNLIMITED, extraBufferCapacity = Channel.UNLIMITED,
) )
val mangaListener = _mangaListener.asSharedFlow() val mangaListener = _mangaListener.asSharedFlow()
private val _chapterIdsListener = MutableSharedFlow<List<Long>>( private val _chapterIdsListener = MutableSharedFlow<List<Long>>(
extraBufferCapacity = Channel.UNLIMITED, extraBufferCapacity = Channel.UNLIMITED,
) )
val chapterIdsListener = _chapterIdsListener.asSharedFlow() val chapterIdsListener = _chapterIdsListener.asSharedFlow()
private val _mangaChapterIdsListener = MutableSharedFlow<List<Long>>( private val _mangaChapterIdsListener = MutableSharedFlow<List<Long>>(
extraBufferCapacity = Channel.UNLIMITED, extraBufferCapacity = Channel.UNLIMITED,
) )
val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow() val mangaChapterIdsListener = _mangaChapterIdsListener.asSharedFlow()
private val categoryMangaListener = MutableSharedFlow<Long>( private val categoryMangaListener = MutableSharedFlow<Long>(
extraBufferCapacity = Channel.UNLIMITED, extraBufferCapacity = Channel.UNLIMITED,
) )
private val extensionListener = MutableSharedFlow<List<String>>( private val extensionListener = MutableSharedFlow<List<String>>(
extraBufferCapacity = Channel.UNLIMITED, extraBufferCapacity = Channel.UNLIMITED,
) )
fun <T> combineMangaUpdates( fun <T> combineMangaUpdates(
flow: Flow<T>, flow: Flow<T>,
predate: (suspend (List<Long>) -> Boolean)? = null, predate: (suspend (List<Long>) -> Boolean)? = null,
) = if (predate != null) { ) = if (predate != null) {
_mangaListener _mangaListener
.filter(predate) .filter(predate)
.startWith(Unit) .startWith(Unit)
} else { } else {
_mangaListener.startWith(Unit) _mangaListener.startWith(Unit)
} }
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
.flatMapLatest { flow } .flatMapLatest { flow }
fun updateManga(vararg ids: Long) { fun updateManga(vararg ids: Long) {
scope.launch { scope.launch {
_mangaListener.emit(ids.toList()) _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 <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()
}
}

View File

@@ -13,30 +13,29 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ExportBackupFile @Inject
@Inject class ExportBackupFile(
constructor( private val backupRepository: BackupRepository,
private val backupRepository: BackupRepository, ) {
) { suspend fun await(
suspend fun await( includeCategories: Boolean,
includeCategories: Boolean, includeChapters: Boolean,
includeChapters: Boolean, block: HttpRequestBuilder.() -> Unit = {},
block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(includeCategories, includeChapters, block)
) = asFlow(includeCategories, includeChapters, block) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to export backup" }
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()
} }
.singleOrNull()
fun asFlow(
includeCategories: Boolean,
includeChapters: Boolean,
block: HttpRequestBuilder.() -> Unit = {},
) = backupRepository.createBackup(includeCategories, includeChapters, block)
companion object {
private val log = logging()
} }
}

View File

@@ -15,24 +15,23 @@ import okio.Path
import okio.SYSTEM import okio.SYSTEM
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ImportBackupFile @Inject
@Inject class ImportBackupFile(
constructor( private val backupRepository: BackupRepository,
private val backupRepository: BackupRepository, ) {
) { suspend fun await(
suspend fun await( file: Path,
file: Path, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(file)
) = asFlow(file) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to import backup ${file.name}" }
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()
} }
.singleOrNull()
fun asFlow(file: Path) = backupRepository.restoreBackup(FileSystem.SYSTEM.source(file))
companion object {
private val log = logging()
} }
}

View File

@@ -15,24 +15,23 @@ import okio.Path
import okio.SYSTEM import okio.SYSTEM
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ValidateBackupFile @Inject
@Inject class ValidateBackupFile(
constructor( private val backupRepository: BackupRepository,
private val backupRepository: BackupRepository, ) {
) { suspend fun await(
suspend fun await( file: Path,
file: Path, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(file)
) = asFlow(file) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to validate backup ${file.name}" }
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()
} }
.singleOrNull()
fun asFlow(file: Path) = backupRepository.validateBackup(FileSystem.SYSTEM.source(file))
companion object {
private val log = logging()
} }
}

View File

@@ -17,61 +17,60 @@ import kotlinx.coroutines.flow.map
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class AddMangaToCategory @Inject
@Inject class AddMangaToCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, categoryId: Long,
categoryId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId, categoryId)
) = asFlow(mangaId, categoryId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to add $mangaId to category $categoryId" }
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)
}
} }
.collect()
fun asFlow( suspend fun await(
manga: Manga, manga: Manga,
category: Category, category: Category,
) = if (category.id != 0L) { onError: suspend (Throwable) -> Unit = {},
categoryRepository.addMangaToCategory(manga.id, category.id) ) = asFlow(manga, category)
.map { serverListeners.updateCategoryManga(category.id) } .catch {
} else { onError(it)
flow { log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" }
serverListeners.updateCategoryManga(category.id)
emit(Unit)
}
} }
.collect()
companion object { fun asFlow(
private val log = logging() 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()
}
}

View File

@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class CreateCategory @Inject
@Inject class CreateCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( name: String,
name: String, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(name)
) = asFlow(name) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to create category $name" }
log.warn(it) { "Failed to create category $name" }
}
.collect()
fun asFlow(name: String) = categoryRepository.createCategory(name)
companion object {
private val log = logging()
} }
.collect()
fun asFlow(name: String) = categoryRepository.createCategory(name)
companion object {
private val log = logging()
} }
}

View File

@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class DeleteCategory @Inject
@Inject class DeleteCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( categoryId: Long,
categoryId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(categoryId)
) = asFlow(categoryId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to delete category $categoryId" }
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()
} }
.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()
} }
}

View File

@@ -13,32 +13,31 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetCategories @Inject
@Inject class GetCategories(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( dropDefault: Boolean = false,
dropDefault: Boolean = false, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(dropDefault)
) = asFlow(dropDefault) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get categories" }
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()
} }
.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()
} }
}

View File

@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetMangaCategories @Inject
@Inject class GetMangaCategories(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get categories for $mangaId" }
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()
} }
.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()
} }
}

View File

@@ -15,45 +15,44 @@ import kotlinx.coroutines.flow.take
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetMangaListFromCategory @Inject
@Inject class GetMangaListFromCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( categoryId: Long,
categoryId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(categoryId)
) = asFlow(categoryId) .take(1)
.take(1) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get manga list from category $categoryId" }
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()
} }
.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()
} }
}

View File

@@ -13,52 +13,51 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ModifyCategory @Inject
@Inject class ModifyCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( categoryId: Long,
categoryId: Long, name: String,
name: String, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(
) = asFlow( categoryId = categoryId,
categoryId = categoryId, name = name,
name = name, ).catch {
).catch { onError(it)
onError(it) log.warn(it) { "Failed to modify category $categoryId with options: name=$name" }
log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } }.collect()
}.collect()
suspend fun await( suspend fun await(
category: Category, category: Category,
name: String? = null, name: String? = null,
onError: suspend (Throwable) -> Unit = {}, onError: suspend (Throwable) -> Unit = {},
) = asFlow( ) = asFlow(
category = category, category = category,
name = name, name = name,
).catch { ).catch {
onError(it) onError(it)
log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" }
}.collect() }.collect()
fun asFlow( fun asFlow(
categoryId: Long, categoryId: Long,
name: String, name: String,
) = categoryRepository.modifyCategory( ) = categoryRepository.modifyCategory(
categoryId = categoryId, categoryId = categoryId,
name = name, name = name,
) )
fun asFlow( fun asFlow(
category: Category, category: Category,
name: String? = null, name: String? = null,
) = categoryRepository.modifyCategory( ) = categoryRepository.modifyCategory(
categoryId = category.id, categoryId = category.id,
name = name ?: category.name, name = name ?: category.name,
) )
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -17,61 +17,60 @@ import kotlinx.coroutines.flow.map
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class RemoveMangaFromCategory @Inject
@Inject class RemoveMangaFromCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, categoryId: Long,
categoryId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId, categoryId)
) = asFlow(mangaId, categoryId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to remove $mangaId from category $categoryId" }
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)
}
} }
.collect()
fun asFlow( suspend fun await(
manga: Manga, manga: Manga,
category: Category, category: Category,
) = if (category.id != 0L) { onError: suspend (Throwable) -> Unit = {},
categoryRepository.removeMangaFromCategory(manga.id, category.id) ) = asFlow(manga, category)
.map { serverListeners.updateCategoryManga(category.id) } .catch {
} else { onError(it)
flow { log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" }
serverListeners.updateCategoryManga(category.id)
emit(Unit)
}
} }
.collect()
companion object { fun asFlow(
private val log = logging() 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()
}
}

View File

@@ -12,28 +12,27 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ReorderCategory @Inject
@Inject class ReorderCategory(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( categoryId: Long,
categoryId: Long, position: Int,
position: Int, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(categoryId, position)
) = asFlow(categoryId, position) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to move category $categoryId to $position" }
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()
} }
.collect()
fun asFlow(
categoryId: Long,
position: Int,
) = categoryRepository.reorderCategory(categoryId, position)
companion object {
private val log = logging()
} }
}

View File

@@ -14,37 +14,36 @@ import kotlinx.coroutines.flow.flow
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateCategoryMeta @Inject
@Inject class UpdateCategoryMeta(
constructor( private val categoryRepository: CategoryRepository,
private val categoryRepository: CategoryRepository, ) {
) { suspend fun await(
suspend fun await( category: Category,
category: Category, example: Int = category.meta.example,
example: Int = category.meta.example, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(category, example)
) = asFlow(category, example) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" }
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)
} }
.collect()
companion object { fun asFlow(
private val log = logging() 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()
}
}

View File

@@ -16,73 +16,72 @@ import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
class DeleteChapterDownload @Inject
@Inject class DeleteChapterDownload(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId)
) = asFlow(chapterId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to delete chapter download for $chapterId" }
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()
} }
.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()
} }
}

View File

@@ -15,47 +15,46 @@ import kotlinx.coroutines.flow.take
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetChapter @Inject
@Inject class GetChapter(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId)
) = asFlow(chapterId) .take(1)
.take(1) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get chapter $chapterId" }
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()
} }
.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()
} }
}

View File

@@ -13,40 +13,39 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetChapterPages @Inject
@Inject class GetChapterPages(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId)
) = asFlow(chapterId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get pages for $chapterId" }
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()
} }
.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()
} }
}

View File

@@ -15,47 +15,46 @@ import kotlinx.coroutines.flow.take
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetChapters @Inject
@Inject class GetChapters(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .take(1)
.take(1) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get chapters for $mangaId" }
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()
} }
.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()
} }
}

View File

@@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class RefreshChapters @Inject
@Inject class RefreshChapters(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to refresh chapters for $mangaId" }
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()
} }
.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()
} }
}

View File

@@ -16,115 +16,114 @@ import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
class UpdateChapter @Inject
@Inject class UpdateChapter(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, bookmarked: Boolean? = null,
bookmarked: Boolean? = null, read: Boolean? = null,
read: Boolean? = null, lastPageRead: Int? = null,
lastPageRead: Int? = null, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId, bookmarked, read, lastPageRead)
) = asFlow(chapterId, bookmarked, read, lastPageRead) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update chapter bookmark for chapter $chapterId" }
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()
} }
.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()
} }
}

View File

@@ -15,51 +15,50 @@ import kotlinx.coroutines.flow.onEach
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateChapterLastPageRead @Inject
@Inject class UpdateChapterLastPageRead(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, lastPageRead: Int,
lastPageRead: Int, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId, lastPageRead)
) = asFlow(chapterId, lastPageRead) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update chapter last page read for chapter $chapterId" }
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()
} }
.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()
} }
}

View File

@@ -15,39 +15,38 @@ import kotlinx.coroutines.flow.flow
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateChapterMeta @Inject
@Inject class UpdateChapterMeta(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( chapter: Chapter,
chapter: Chapter, pageOffset: Int = chapter.meta.juiPageOffset,
pageOffset: Int = chapter.meta.juiPageOffset, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapter, pageOffset)
) = asFlow(chapter, pageOffset) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" }
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)
} }
.collect()
companion object { fun asFlow(
private val log = logging() 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()
}
}

View File

@@ -15,59 +15,58 @@ import kotlinx.coroutines.flow.onEach
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateChapterRead @Inject
@Inject class UpdateChapterRead(
constructor( private val chapterRepository: ChapterRepository,
private val chapterRepository: ChapterRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, read: Boolean,
read: Boolean, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId, read)
) = asFlow(chapterId, read) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update chapter read status for chapter $chapterId" }
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()
} }
.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()
} }
}

View File

@@ -12,36 +12,35 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class BatchChapterDownload @Inject
@Inject class BatchChapterDownload(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(
suspend fun await( chapterIds: List<Long>,
chapterIds: List<Long>, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterIds)
) = asFlow(chapterIds) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to queue chapters $chapterIds for a download" }
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()
} }
.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()
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ClearDownloadQueue @Inject
@Inject class ClearDownloadQueue(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to clear download queue" }
log.warn(it) { "Failed to clear download queue" } }
} .collect()
.collect()
fun asFlow() = downloadRepository.clearDownloadQueue() fun asFlow() = downloadRepository.clearDownloadQueue()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class QueueChapterDownload @Inject
@Inject class QueueChapterDownload(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId)
) = asFlow(chapterId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to queue chapter $chapterId for a download" }
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()
} }
.collect()
fun asFlow(chapterId: Long) = downloadRepository.queueChapterDownload(chapterId)
companion object {
private val log = logging()
} }
}

View File

@@ -12,28 +12,27 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ReorderChapterDownload @Inject
@Inject class ReorderChapterDownload(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, to: Int,
to: Int, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId, to)
) = asFlow(chapterId, to) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to reorder chapter download for $chapterId to $to" }
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()
} }
.collect()
fun asFlow(
chapterId: Long,
to: Int,
) = downloadRepository.reorderChapterDownload(chapterId, to)
companion object {
private val log = logging()
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class StartDownloading @Inject
@Inject class StartDownloading(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to start downloader" }
log.warn(it) { "Failed to start downloader" } }
} .collect()
.collect()
fun asFlow() = downloadRepository.startDownloading() fun asFlow() = downloadRepository.startDownloading()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class StopChapterDownload @Inject
@Inject class StopChapterDownload(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(
suspend fun await( chapterId: Long,
chapterId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(chapterId)
) = asFlow(chapterId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to stop chapter download for $chapterId" }
log.warn(it) { "Failed to stop chapter download for $chapterId" }
}
.collect()
fun asFlow(chapterId: Long) = downloadRepository.stopChapterDownload(chapterId)
companion object {
private val log = logging()
} }
.collect()
fun asFlow(chapterId: Long) = downloadRepository.stopChapterDownload(chapterId)
companion object {
private val log = logging()
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class StopDownloading @Inject
@Inject class StopDownloading(
constructor( private val downloadRepository: DownloadRepository,
private val downloadRepository: DownloadRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to stop downloader" }
log.warn(it) { "Failed to stop downloader" } }
} .collect()
.collect()
fun asFlow() = downloadRepository.stopDownloading() fun asFlow() = downloadRepository.stopDownloading()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -16,42 +16,40 @@ import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class DownloadService @Inject
@Inject class DownloadService(
constructor( serverPreferences: ServerPreferences,
serverPreferences: ServerPreferences, client: Http,
client: Http, ) : WebsocketService(serverPreferences, client) {
) : WebsocketService(serverPreferences, client) { override val _status: MutableStateFlow<Status>
override val _status: MutableStateFlow<Status> get() = status
get() = status
override val query: String override val query: String
get() = "/api/v1/downloads" get() = "/api/v1/downloads"
override suspend fun onReceived(frame: Frame.Text) { override suspend fun onReceived(frame: Frame.Text) {
val status = json.decodeFromString<DownloadStatus>(frame.readText()) val status = json.decodeFromString<DownloadStatus>(frame.readText())
downloaderStatus.value = status.status downloaderStatus.value = status.status
downloadQueue.value = status.queue 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 }
}
}
} }
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 }
}
}
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetExtensionList @Inject
@Inject class GetExtensionList(
constructor( private val extensionRepository: ExtensionRepository,
private val extensionRepository: ExtensionRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get extension list" }
log.warn(it) { "Failed to get extension list" } }
} .singleOrNull()
.singleOrNull()
fun asFlow() = extensionRepository.getExtensionList() fun asFlow() = extensionRepository.getExtensionList()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class InstallExtension @Inject
@Inject class InstallExtension(
constructor( private val extensionRepository: ExtensionRepository,
private val extensionRepository: ExtensionRepository, ) {
) { suspend fun await(
suspend fun await( extension: Extension,
extension: Extension, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(extension)
) = asFlow(extension) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to install extension ${extension.apkName}" }
log.warn(it) { "Failed to install extension ${extension.apkName}" }
}
.collect()
fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName)
companion object {
private val log = logging()
} }
.collect()
fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName)
companion object {
private val log = logging()
} }
}

View File

@@ -15,24 +15,23 @@ import okio.Path
import okio.SYSTEM import okio.SYSTEM
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class InstallExtensionFile @Inject
@Inject class InstallExtensionFile(
constructor( private val extensionRepository: ExtensionRepository,
private val extensionRepository: ExtensionRepository, ) {
) { suspend fun await(
suspend fun await( path: Path,
path: Path, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(path)
) = asFlow(path) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to install extension from $path" }
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()
} }
.collect()
fun asFlow(path: Path) = extensionRepository.installExtension(FileSystem.SYSTEM.source(path))
companion object {
private val log = logging()
} }
}

View File

@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UninstallExtension @Inject
@Inject class UninstallExtension(
constructor( private val extensionRepository: ExtensionRepository,
private val extensionRepository: ExtensionRepository, ) {
) { suspend fun await(
suspend fun await( extension: Extension,
extension: Extension, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(extension)
) = asFlow(extension) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to uninstall extension ${extension.apkName}" }
log.warn(it) { "Failed to uninstall extension ${extension.apkName}" }
}
.collect()
fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName)
companion object {
private val log = logging()
} }
.collect()
fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName)
companion object {
private val log = logging()
} }
}

View File

@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateExtension @Inject
@Inject class UpdateExtension(
constructor( private val extensionRepository: ExtensionRepository,
private val extensionRepository: ExtensionRepository, ) {
) { suspend fun await(
suspend fun await( extension: Extension,
extension: Extension, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(extension)
) = asFlow(extension) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update extension ${extension.apkName}" }
log.warn(it) { "Failed to update extension ${extension.apkName}" }
}
.collect()
fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName)
companion object {
private val log = logging()
} }
.collect()
fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName)
companion object {
private val log = logging()
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetGlobalMeta @Inject
@Inject class GetGlobalMeta(
constructor( private val globalRepository: GlobalRepository,
private val globalRepository: GlobalRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get global meta" }
log.warn(it) { "Failed to get global meta" } }
} .singleOrNull()
.singleOrNull()
fun asFlow() = globalRepository.getGlobalMeta() fun asFlow() = globalRepository.getGlobalMeta()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -14,36 +14,35 @@ import kotlinx.coroutines.flow.flow
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateGlobalMeta @Inject
@Inject class UpdateGlobalMeta(
constructor( private val globalRepository: GlobalRepository,
private val globalRepository: GlobalRepository, ) {
) { suspend fun await(
suspend fun await( globalMeta: GlobalMeta,
globalMeta: GlobalMeta, example: Int = globalMeta.example,
example: Int = globalMeta.example, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(globalMeta, example)
) = asFlow(globalMeta, example) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update global meta" }
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)
} }
.collect()
companion object { fun asFlow(
private val log = logging() 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()
}
}

View File

@@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class AddMangaToLibrary @Inject
@Inject class AddMangaToLibrary(
constructor( private val libraryRepository: LibraryRepository,
private val libraryRepository: LibraryRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to add $mangaId to library" }
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()
} }
.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()
} }
}

View File

@@ -15,41 +15,40 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class RemoveMangaFromLibrary @Inject
@Inject class RemoveMangaFromLibrary(
constructor( private val libraryRepository: LibraryRepository,
private val libraryRepository: LibraryRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to remove $mangaId from library" }
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()
} }
.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()
} }
}

View File

@@ -16,26 +16,25 @@ import kotlinx.coroutines.flow.MutableStateFlow
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class LibraryUpdateService @Inject
@Inject class LibraryUpdateService(
constructor( serverPreferences: ServerPreferences,
serverPreferences: ServerPreferences, client: Http,
client: Http, ) : WebsocketService(serverPreferences, client) {
) : WebsocketService(serverPreferences, client) { override val _status: MutableStateFlow<Status>
override val _status: MutableStateFlow<Status> get() = status
get() = status
override val query: String override val query: String
get() = "/api/v1/update" get() = "/api/v1/update"
override suspend fun onReceived(frame: Frame.Text) { override suspend fun onReceived(frame: Frame.Text) {
updateStatus.value = json.decodeFromString<UpdateStatus>(frame.readText()) 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))
}
} }
companion object {
private val log = logging()
val status = MutableStateFlow(Status.STARTING)
val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), emptyMap(), false))
}
}

View File

@@ -15,45 +15,44 @@ import kotlinx.coroutines.flow.take
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetManga @Inject
@Inject class GetManga(
constructor( private val mangaRepository: MangaRepository,
private val mangaRepository: MangaRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .take(1)
.take(1) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get manga $mangaId" }
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()
} }
.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()
} }
}

View File

@@ -16,39 +16,38 @@ import kotlinx.coroutines.flow.take
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class RefreshManga @Inject
@Inject class RefreshManga(
constructor( private val mangaRepository: MangaRepository,
private val mangaRepository: MangaRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( mangaId: Long,
mangaId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(mangaId)
) = asFlow(mangaId) .take(1)
.take(1) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to refresh manga $mangaId" }
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()
} }
.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()
} }
}

View File

@@ -17,39 +17,38 @@ import kotlinx.coroutines.flow.flow
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateMangaMeta @Inject
@Inject class UpdateMangaMeta(
constructor( private val mangaRepository: MangaRepository,
private val mangaRepository: MangaRepository, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) {
) { suspend fun await(
suspend fun await( manga: Manga,
manga: Manga, readerMode: String = manga.meta.juiReaderMode,
readerMode: String = manga.meta.juiReaderMode, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(manga, readerMode)
) = asFlow(manga, readerMode) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" }
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)
} }
.collect()
companion object { fun asFlow(
private val log = logging() 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()
}
}

View File

@@ -11,20 +11,19 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences
import ca.gosyer.jui.domain.reader.service.ReaderPreferences import ca.gosyer.jui.domain.reader.service.ReaderPreferences
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class RunMigrations @Inject
@Inject class RunMigrations(
constructor( private val migrationPreferences: MigrationPreferences,
private val migrationPreferences: MigrationPreferences, private val readerPreferences: ReaderPreferences,
private val readerPreferences: ReaderPreferences, ) {
) { fun runMigrations() {
fun runMigrations() { val code = migrationPreferences.version().get()
val code = migrationPreferences.version().get() if (code <= 0) {
if (code <= 0) { readerPreferences.modes().get().forEach {
readerPreferences.modes().get().forEach { readerPreferences.getMode(it).direction().delete()
readerPreferences.getMode(it).direction().delete()
}
migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE)
return
} }
migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE)
return
} }
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class AboutServer @Inject
@Inject class AboutServer(
constructor( private val settingsRepository: SettingsRepository,
private val settingsRepository: SettingsRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get server information" }
log.warn(it) { "Failed to get server information" } }
} .singleOrNull()
.singleOrNull()
fun asFlow() = settingsRepository.aboutServer() fun asFlow() = settingsRepository.aboutServer()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetSettings @Inject
@Inject class GetSettings(
constructor( private val settingsRepository: SettingsRepository,
private val settingsRepository: SettingsRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to check for server updates" }
log.warn(it) { "Failed to check for server updates" } }
} .singleOrNull()
.singleOrNull()
fun asFlow() = settingsRepository.getSettings() fun asFlow() = settingsRepository.getSettings()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -13,24 +13,23 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class SetSettings @Inject
@Inject class SetSettings(
constructor( private val settingsRepository: SettingsRepository,
private val settingsRepository: SettingsRepository, ) {
) { suspend fun await(
suspend fun await( input: SetSettingsInput,
input: SetSettingsInput, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(input)
) = asFlow(input) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to check for server updates" }
log.warn(it) { "Failed to check for server updates" }
}
.singleOrNull()
fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input)
companion object {
private val log = logging()
} }
.singleOrNull()
fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input)
companion object {
private val log = logging()
} }
}

View File

@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetFilterList @Inject
@Inject class GetFilterList(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(
suspend fun await( source: Source,
source: Source, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(source.id)
) = asFlow(source.id) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get filter list for ${source.displayName}" }
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()
} }
.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()
} }
}

View File

@@ -13,44 +13,43 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetLatestManga @Inject
@Inject class GetLatestManga(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(
suspend fun await( source: Source,
source: Source, page: Int,
page: Int, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(source.id, page)
) = asFlow(source.id, page) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" }
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()
} }
.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()
} }
}

View File

@@ -13,44 +13,43 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetPopularManga @Inject
@Inject class GetPopularManga(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(
suspend fun await( source: Source,
source: Source, page: Int,
page: Int, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(source.id, page)
) = asFlow(source.id, page) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" }
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()
} }
.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()
} }
}

View File

@@ -14,64 +14,63 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetSearchManga @Inject
@Inject class GetSearchManga(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(
suspend fun await( source: Source,
source: Source, page: Int,
page: Int, searchTerm: String?,
searchTerm: String?, filters: List<SourceFilter>?,
filters: List<SourceFilter>?, onError: suspend (Throwable) -> Unit = {
onError: suspend (Throwable) -> Unit = { },
}, ) = asFlow(source.id, page, searchTerm, filters)
) = asFlow(source.id, page, searchTerm, filters) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" }
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()
} }
.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()
} }
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetSourceList @Inject
@Inject class GetSourceList(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get source list" }
log.warn(it) { "Failed to get source list" } }
} .singleOrNull()
.singleOrNull()
fun asFlow() = sourceRepository.getSourceList() fun asFlow() = sourceRepository.getSourceList()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetSourceSettings @Inject
@Inject class GetSourceSettings(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(
suspend fun await( source: Source,
source: Source, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(source.id)
) = asFlow(source.id) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get source settings for ${source.displayName}" }
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()
} }
.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()
} }
}

View File

@@ -13,32 +13,31 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class SetSourceSetting @Inject
@Inject class SetSourceSetting(
constructor( private val sourceRepository: SourceRepository,
private val sourceRepository: SourceRepository, ) {
) { suspend fun await(
suspend fun await( sourceId: Long,
sourceId: Long, sourcePreference: SourcePreference,
sourcePreference: SourcePreference, onError: suspend (Throwable) -> Unit = {
onError: suspend (Throwable) -> Unit = { },
}, ) = asFlow(sourceId, sourcePreference)
) = asFlow(sourceId, sourcePreference) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to set setting for $sourceId with index = ${sourcePreference.position}" }
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()
} }
.collect()
fun asFlow(
sourceId: Long,
sourcePreference: SourcePreference,
) = sourceRepository.setSourceSetting(
sourceId,
sourcePreference,
)
companion object {
private val log = logging()
} }
}

View File

@@ -33,21 +33,21 @@ fun interface GetMangaPage {
suspend fun get(page: Int): MangaPage? suspend fun get(page: Int): MangaPage?
} }
class SourcePager @Inject
@Inject class SourcePager(
constructor( private val getManga: GetManga,
private val getManga: GetManga, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, @Assisted private val fetcher: GetMangaPage,
@Assisted private val fetcher: GetMangaPage, ) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { private val sourceMutex = Mutex()
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 } } private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } }
.stateIn(this, SharingStarted.Eagerly, emptyList()) .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 { coroutineScope {
manga + updatedMangaIds.filter { it in mangaIds.value }.map { manga + updatedMangaIds.filter { it in mangaIds.value }.map {
async { async {
@@ -57,37 +57,37 @@ class SourcePager
} }
}.stateIn(this, SharingStarted.Eagerly, emptyMap()) }.stateIn(this, SharingStarted.Eagerly, emptyMap())
val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga -> val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga ->
sourceManga.map { changedManga[it.id] ?: it } sourceManga.map { changedManga[it.id] ?: it }
}.stateIn(this, SharingStarted.Eagerly, emptyList()) }.stateIn(this, SharingStarted.Eagerly, emptyList())
private val _pageNum = MutableStateFlow(0) private val _pageNum = MutableStateFlow(0)
val pageNum = _pageNum.asStateFlow() val pageNum = _pageNum.asStateFlow()
private val _hasNextPage = MutableStateFlow(true) private val _hasNextPage = MutableStateFlow(true)
val hasNextPage = _hasNextPage.asStateFlow() val hasNextPage = _hasNextPage.asStateFlow()
private val _loading = MutableStateFlow(true) private val _loading = MutableStateFlow(true)
val loading = _loading.asStateFlow() val loading = _loading.asStateFlow()
fun loadNextPage() { fun loadNextPage() {
launch { launch {
if (hasNextPage.value && sourceMutex.tryLock()) { if (hasNextPage.value && sourceMutex.tryLock()) {
_pageNum.value++ _pageNum.value++
val page = fetcher.get(_pageNum.value) val page = fetcher.get(_pageNum.value)
if (page != null) { if (page != null) {
_sourceManga.value = _sourceManga.value + page.mangaList _sourceManga.value = _sourceManga.value + page.mangaList
_hasNextPage.value = page.hasNextPage _hasNextPage.value = page.hasNextPage
} else { } else {
_pageNum.value-- _pageNum.value--
}
sourceMutex.unlock()
} }
_loading.value = false sourceMutex.unlock()
} }
} _loading.value = false
companion object {
private val log = logging()
} }
} }
companion object {
private val log = logging()
}
}

View File

@@ -15,7 +15,7 @@ enum class StartScreen {
Library, Library,
Updates, Updates,
// History, // History,
Sources, Sources,
Extensions, Extensions,
} }

View File

@@ -12,24 +12,23 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GetRecentUpdates @Inject
@Inject class GetRecentUpdates(
constructor( private val updatesRepository: UpdatesRepository,
private val updatesRepository: UpdatesRepository, ) {
) { suspend fun await(
suspend fun await( pageNum: Int,
pageNum: Int, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(pageNum)
) = asFlow(pageNum) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to get updates for page $pageNum" }
log.warn(it) { "Failed to get updates for page $pageNum" }
}
.singleOrNull()
fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum)
companion object {
private val log = logging()
} }
.singleOrNull()
fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum)
companion object {
private val log = logging()
} }
}

View File

@@ -13,36 +13,35 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateCategory @Inject
@Inject class UpdateCategory(
constructor( private val updatesRepository: UpdatesRepository,
private val updatesRepository: UpdatesRepository, ) {
) { suspend fun await(
suspend fun await( categoryId: Long,
categoryId: Long, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(categoryId)
) = asFlow(categoryId) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update category $categoryId" }
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()
} }
.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()
} }
}

View File

@@ -21,77 +21,76 @@ import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateChecker @Inject
@Inject class UpdateChecker(
constructor( private val updatePreferences: UpdatePreferences,
private val updatePreferences: UpdatePreferences, private val client: Http,
private val client: Http, ) {
) { suspend fun await(
suspend fun await( manualFetch: Boolean,
manualFetch: Boolean, onError: suspend (Throwable) -> Unit = {},
onError: suspend (Throwable) -> Unit = {}, ) = asFlow(manualFetch)
) = asFlow(manualFetch) .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to check for updates" }
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()
} }
.singleOrNull()
// Thanks to Tachiyomi for inspiration fun asFlow(manualFetch: Boolean) =
private fun isNewVersion(versionTag: String): Boolean { flow {
// Removes prefixes like "r" or "v" if (!manualFetch && !updatePreferences.enabled().get()) return@flow
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") val latestRelease = client.get(
"https://api.github.com/repos/$GITHUB_REPO/releases/latest",
).body<GithubRelease>()
return if (BuildKonfig.IS_PREVIEW) { if (isNewVersion(latestRelease.version)) {
// Preview builds: based on releases in "Suwayomi/Suwayomi-JUI-preview" repo emit(Update.UpdateFound(latestRelease))
// tagged as something like "r123"
newVersion.toInt() > BuildKonfig.PREVIEW_BUILD
} else { } else {
// Release builds: based on releases in "Suwayomi/Suwayomi-JUI" repo emit(Update.NoUpdatesFound)
// tagged as something like "v1.1.2"
newVersion != BuildKonfig.VERSION
} }
} }.flowOn(Dispatchers.IO)
companion object { sealed class Update {
private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) { data class UpdateFound(
"Suwayomi/Suwayomi-JUI-preview" val release: GithubRelease,
} else { ) : Update()
"Suwayomi/Suwayomi-JUI"
}
private val RELEASE_TAG: String by lazy { data object NoUpdatesFound : Update()
if (BuildKonfig.IS_PREVIEW) { }
"r${BuildKonfig.PREVIEW_BUILD}"
} else {
"v${BuildKonfig.VERSION}"
}
}
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()
}
}

View File

@@ -12,22 +12,21 @@ import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdateLibrary @Inject
@Inject class UpdateLibrary(
constructor( private val updatesRepository: UpdatesRepository,
private val updatesRepository: UpdatesRepository, ) {
) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow()
asFlow() .catch {
.catch { onError(it)
onError(it) log.warn(it) { "Failed to update library" }
log.warn(it) { "Failed to update library" } }
} .collect()
.collect()
fun asFlow() = updatesRepository.updateLibrary() fun asFlow() = updatesRepository.updateLibrary()
companion object { companion object {
private val log = logging() private val log = logging()
}
} }
}

View File

@@ -37,54 +37,54 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class UpdatesPager @Inject
@Inject class UpdatesPager(
constructor( private val getRecentUpdates: GetRecentUpdates,
private val getRecentUpdates: GetRecentUpdates, private val getManga: GetManga,
private val getManga: GetManga, private val getChapter: GetChapter,
private val getChapter: GetChapter, private val serverListeners: ServerListeners,
private val serverListeners: ServerListeners, ) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) {
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { private val updatesMutex = Mutex()
private val updatesMutex = Mutex()
private val fetchedUpdates = MutableSharedFlow<List<MangaAndChapter>>() private val fetchedUpdates = MutableSharedFlow<List<MangaAndChapter>>()
private val foldedUpdates = fetchedUpdates.runningFold(emptyList<Updates>()) { updates, newUpdates -> private val foldedUpdates = fetchedUpdates.runningFold(emptyList<Updates>()) { updates, newUpdates ->
updates.ifEmpty { updates.ifEmpty {
val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates
listOf( listOf(
Updates.Date( Updates.Date(
Instant.fromEpochSeconds(first.fetchedAt) 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)
.toLocalDateTime(TimeZone.currentSystemDefault()) .toLocalDateTime(TimeZone.currentSystemDefault())
.date .date,
val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt) ),
.toLocalDateTime(TimeZone.currentSystemDefault()) )
.date } + newUpdates.fold(emptyList()) { list, (manga, chapter) ->
chapterDate.takeUnless { it == lastUpdateDate } val date = (list.lastOrNull() as? Updates.Update)?.let {
} val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt)
.toLocalDateTime(TimeZone.currentSystemDefault())
if (date == null) { .date
list + Updates.Update(manga, chapter) val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt)
} else { .toLocalDateTime(TimeZone.currentSystemDefault())
list + Updates.Date(date) + Updates.Update(manga, chapter) .date
} chapterDate.takeUnless { it == lastUpdateDate }
} }
}.stateIn(this, SharingStarted.Eagerly, emptyList())
private val mangaIds = foldedUpdates.map { updates -> if (date == null) {
updates.filterIsInstance<Updates.Update>().map { it.manga.id } list + Updates.Update(manga, chapter)
}.stateIn(this, SharingStarted.Eagerly, emptyList()) } else {
private val chapterIds = foldedUpdates.map { updates -> list + Updates.Date(date) + Updates.Update(manga, chapter)
updates.filterIsInstance<Updates.Update>().map { it.chapter.id } }
}.stateIn(this, SharingStarted.Eagerly, emptyList()) }
}.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 { coroutineScope {
manga + updatedMangaIds.filter { it in mangaIds.value }.map { manga + updatedMangaIds.filter { it in mangaIds.value }.map {
async { async {
@@ -94,82 +94,82 @@ class UpdatesPager
} }
}.stateIn(this, SharingStarted.Eagerly, emptyMap()) }.stateIn(this, SharingStarted.Eagerly, emptyMap())
private val changedChapters = MutableStateFlow(emptyMap<Long, Chapter>()) private val changedChapters = MutableStateFlow(emptyMap<Long, Chapter>())
init { init {
serverListeners.chapterIdsListener serverListeners.chapterIdsListener
.onEach { updatedChapterIds -> .onEach { updatedChapterIds ->
val chapters = coroutineScope { val chapters = coroutineScope {
updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it == id } }.map { updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it == id } }.map {
async { async {
getChapter.await(it) getChapter.await(it)
} }
}.awaitAll().filterNotNull().associateBy { it.id } }.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,
)
} }
changedChapters.update { it + chapters }
} }
}.stateIn(this, SharingStarted.Eagerly, emptyList()) .launchIn(this)
}
private val currentPage = MutableStateFlow(0) val updates = combine(
private val hasNextPage = MutableStateFlow(true) 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 @Immutable
sealed class Updates { data class Date(
@Immutable val date: String,
data class Update( ) : Updates() {
val manga: Manga, constructor(date: LocalDate) : this(date.toString())
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
} }
} }
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
}
}

View File

@@ -38,169 +38,168 @@ import kotlin.io.path.exists
import kotlin.io.path.isExecutable import kotlin.io.path.isExecutable
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
class ServerService @Inject
@Inject class ServerService(
constructor( private val serverHostPreferences: ServerHostPreferences,
private val serverHostPreferences: ServerHostPreferences, ) {
) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val host = serverHostPreferences.host().stateIn(GlobalScope) private val host = serverHostPreferences.host().stateIn(GlobalScope)
private val _initialized = MutableStateFlow( private val _initialized = MutableStateFlow(
if (host.value) { if (host.value) {
ServerResult.STARTING ServerResult.STARTING
} else { } else {
ServerResult.UNUSED ServerResult.UNUSED
}, },
) )
val initialized = _initialized.asStateFlow() val initialized = _initialized.asStateFlow()
private var process: Process? = null private var process: Process? = null
fun startAnyway() { fun startAnyway() {
_initialized.value = ServerResult.UNUSED _initialized.value = ServerResult.UNUSED
} }
@Throws(IOException::class) @Throws(IOException::class)
private suspend fun copyJar(jarFile: Path) { private suspend fun copyJar(jarFile: Path) {
javaClass.getResourceAsStream("/Tachidesk.jar")?.source() javaClass.getResourceAsStream("/Tachidesk.jar")?.source()
?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer()) ?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer())
} }
private fun getJavaFromPath(javaPath: Path): String? { private fun getJavaFromPath(javaPath: Path): String? {
val javaExeFile = javaPath.resolve("java.exe").toNioPath() val javaExeFile = javaPath.resolve("java.exe").toNioPath()
val javaUnixFile = javaPath.resolve("java").toNioPath() val javaUnixFile = javaPath.resolve("java").toNioPath()
return when { return when {
javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString() javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString()
javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString() javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString()
else -> null 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 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()
}
}

View File

@@ -15,8 +15,7 @@ import me.tatarka.inject.annotations.Inject
@Composable @Composable
actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {} actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {}
actual class SettingsServerHostViewModel @Inject
@Inject actual class SettingsServerHostViewModel(
constructor( contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper)
) : ViewModel(contextWrapper)

View File

@@ -30,104 +30,103 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class ImageLoaderProvider @Inject
@Inject class ImageLoaderProvider(
constructor( private val http: Http,
private val http: Http, serverPreferences: ServerPreferences,
serverPreferences: ServerPreferences, private val context: ContextWrapper,
private val context: ContextWrapper, ) {
) { @OptIn(DelicateCoroutinesApi::class)
@OptIn(DelicateCoroutinesApi::class) val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
fun get(imageCache: ImageCache): ImageLoader = fun get(imageCache: ImageCache): ImageLoader =
ImageLoader { ImageLoader {
components { components {
register(context, http) register(context, http)
add(MokoResourceFetcher.Factory()) add(MokoResourceFetcher.Factory())
add(MangaCoverMapper()) add(MangaCoverMapper())
add(MangaCoverKeyer()) add(MangaCoverKeyer())
add(ExtensionIconMapper()) add(ExtensionIconMapper())
add(ExtensionIconKeyer()) add(ExtensionIconKeyer())
add(SourceIconMapper()) add(SourceIconMapper())
add(SourceIconKeyer()) add(SourceIconKeyer())
}
options {
configure(context)
}
interceptor {
diskCache { imageCache }
bitmapMemoryCacheConfig { configure(context) }
}
} }
options {
inner class MangaCoverMapper : Mapper<Url> { configure(context)
override fun map( }
data: Any, interceptor {
options: Options, diskCache { imageCache }
): Url? { bitmapMemoryCacheConfig { configure(context) }
if (data !is Manga) return null
if (data.thumbnailUrl.isNullOrBlank()) return null
return Url(serverUrl.value.toString() + data.thumbnailUrl)
} }
} }
class MangaCoverKeyer : Keyer { inner class MangaCoverMapper : Mapper<Url> {
override fun key( override fun map(
data: Any, data: Any,
options: Options, options: Options,
type: Keyer.Type, ): Url? {
): String? { if (data !is Manga) return null
if (data !is Manga) return null if (data.thumbnailUrl.isNullOrBlank()) return null
return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" return Url(serverUrl.value.toString() + data.thumbnailUrl)
}
}
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
}
} }
} }
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 OptionsBuilder.configure(contextWrapper: ContextWrapper)
expect fun ComponentRegistryBuilder.register( expect fun ComponentRegistryBuilder.register(

View File

@@ -59,100 +59,102 @@ fun AppTheme(content: @Composable () -> Unit) {
} }
} }
class AppThemeViewModel @Inject
@Inject class AppThemeViewModel(
constructor( private val uiPreferences: UiPreferences,
private val uiPreferences: UiPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { override val scope = MainScope()
override val scope = MainScope()
private val themeMode = uiPreferences.themeMode().asStateFlow() private val themeMode = uiPreferences.themeMode().asStateFlow()
private val lightTheme = uiPreferences.lightTheme().asStateFlow() private val lightTheme = uiPreferences.lightTheme().asStateFlow()
private val darkTheme = uiPreferences.darkTheme().asStateFlow() private val darkTheme = uiPreferences.darkTheme().asStateFlow()
private val baseThemeJob = SupervisorJob() private val baseThemeJob = SupervisorJob()
private val baseThemeScope = CoroutineScope(baseThemeJob) private val baseThemeScope = CoroutineScope(baseThemeJob)
@Composable @Composable
fun getColors(): Pair<Colors, ExtraColors> { fun getColors(): Pair<Colors, ExtraColors> {
val themeMode by themeMode.collectAsState() val themeMode by themeMode.collectAsState()
val lightTheme by lightTheme.collectAsState() val lightTheme by lightTheme.collectAsState()
val darkTheme by darkTheme.collectAsState() val darkTheme by darkTheme.collectAsState()
val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme) val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme)
val colors = remember(baseTheme.colors.isLight) { val colors = remember(baseTheme.colors.isLight) {
baseThemeJob.cancelChildren() baseThemeJob.cancelChildren()
if (baseTheme.colors.isLight) { if (baseTheme.colors.isLight) {
uiPreferences.getLightColors().asStateFlow(baseThemeScope) uiPreferences.getLightColors().asStateFlow(baseThemeScope)
} else { } else {
uiPreferences.getDarkColors().asStateFlow(baseThemeScope) 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)
} }
} }
private fun getMaterialColors( val primary by colors.primaryStateFlow.collectAsState()
baseColors: Colors, val secondary by colors.secondaryStateFlow.collectAsState()
colorPrimary: Color, val tertiary by colors.tertiaryStateFlow.collectAsState()
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( return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors(
baseExtraColors: ExtraColors, baseTheme.extraColors,
colorTertiary: Color, tertiary,
): ExtraColors { )
val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary } }
return baseExtraColors.copy(
tertiary = tertiary,
)
}
override fun onDispose() { @Composable
baseThemeScope.cancel() private fun getBaseTheme(
scope.cancel() 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()
}
}

View File

@@ -27,121 +27,126 @@ import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class CategoriesScreenViewModel @Inject
@Inject class CategoriesScreenViewModel(
constructor( private val getCategories: GetCategories,
private val getCategories: GetCategories, private val createCategory: CreateCategory,
private val createCategory: CreateCategory, private val deleteCategory: DeleteCategory,
private val deleteCategory: DeleteCategory, private val modifyCategory: ModifyCategory,
private val modifyCategory: ModifyCategory, private val reorderCategory: ReorderCategory,
private val reorderCategory: ReorderCategory, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private var originalCategories = emptyList<Category>()
private var originalCategories = emptyList<Category>() private val _categories = MutableStateFlow<ImmutableList<MenuCategory>>(persistentListOf())
private val _categories = MutableStateFlow<ImmutableList<MenuCategory>>(persistentListOf()) val categories = _categories.asStateFlow()
val categories = _categories.asStateFlow()
init { init {
scope.launch { scope.launch {
getCategories() 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()
} }
} }
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()
}
}

View File

@@ -31,85 +31,90 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class DownloadsScreenViewModel @Inject
@Inject class DownloadsScreenViewModel(
constructor( private val downloadService: DownloadService,
private val downloadService: DownloadService, private val startDownloading: StartDownloading,
private val startDownloading: StartDownloading, private val stopDownloading: StopDownloading,
private val stopDownloading: StopDownloading, private val clearDownloadQueue: ClearDownloadQueue,
private val clearDownloadQueue: ClearDownloadQueue, private val queueChapterDownload: QueueChapterDownload,
private val queueChapterDownload: QueueChapterDownload, private val stopChapterDownload: StopChapterDownload,
private val stopChapterDownload: StopChapterDownload, private val reorderChapterDownload: ReorderChapterDownload,
private val reorderChapterDownload: ReorderChapterDownload, private val contextWrapper: ContextWrapper,
private val contextWrapper: ContextWrapper, @Assisted standalone: Boolean,
@Assisted standalone: Boolean, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val uiScope = if (standalone) {
private val uiScope = if (standalone) { MainScope()
MainScope() } else {
} else { null
null }
}
override val scope: CoroutineScope override val scope: CoroutineScope
get() = uiScope ?: super.scope get() = uiScope ?: super.scope
val serviceStatus = DownloadService.status.asStateFlow() val serviceStatus = DownloadService.status.asStateFlow()
val downloaderStatus = DownloadService.downloaderStatus.asStateFlow() val downloaderStatus = DownloadService.downloaderStatus.asStateFlow()
val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() } val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() }
.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) .stateIn(scope, SharingStarted.Eagerly, persistentListOf())
fun start() { fun start() {
scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) } scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) }
} }
fun pause() { fun pause() {
scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) } scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) }
} }
fun clear() { fun clear() {
scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) } scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) }
} }
fun stopDownload(chapter: Chapter) { fun stopDownload(chapter: Chapter) {
scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) } scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) }
} }
fun moveUp(chapter: Chapter) { fun moveUp(chapter: Chapter) {
scope.launch { scope.launch {
val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } val index =
if (index == -1 || index <= 0) return@launch downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index }
reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) }) 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 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()
}
}

View File

@@ -41,188 +41,187 @@ import okio.Source
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
import kotlin.random.Random import kotlin.random.Random
class ExtensionsScreenViewModel @Inject
@Inject class ExtensionsScreenViewModel(
constructor( private val getExtensionList: GetExtensionList,
private val getExtensionList: GetExtensionList, private val installExtensionFile: InstallExtensionFile,
private val installExtensionFile: InstallExtensionFile, private val installExtension: InstallExtension,
private val installExtension: InstallExtension, private val updateExtension: UpdateExtension,
private val updateExtension: UpdateExtension, private val uninstallExtension: UninstallExtension,
private val uninstallExtension: UninstallExtension, extensionPreferences: ExtensionPreferences,
extensionPreferences: ExtensionPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val extensionList = MutableStateFlow<List<Extension>?>(null)
private val extensionList = MutableStateFlow<List<Extension>?>(null)
private val _enabledLangs = extensionPreferences.languages().asStateFlow() private val _enabledLangs = extensionPreferences.languages().asStateFlow()
val enabledLangs = _enabledLangs.map { it.toImmutableSet() } val enabledLangs = _enabledLangs.map { it.toImmutableSet() }
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) .stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
private val _searchQuery = MutableStateFlow<String?>(null) private val _searchQuery = MutableStateFlow<String?>(null)
val searchQuery = _searchQuery.asStateFlow() val searchQuery = _searchQuery.asStateFlow()
private val workingExtensions = MutableStateFlow<List<String>>(emptyList()) private val workingExtensions = MutableStateFlow<List<String>>(emptyList())
val extensions = combine( val extensions = combine(
searchQuery, searchQuery,
extensionList, extensionList,
enabledLangs, enabledLangs,
workingExtensions, workingExtensions,
) { searchQuery, extensions, enabledLangs, workingExtensions -> ) { searchQuery, extensions, enabledLangs, workingExtensions ->
search(searchQuery, extensions, enabledLangs, workingExtensions) search(searchQuery, extensions, enabledLangs, workingExtensions)
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) }.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
val availableLangs = extensionList.filterNotNull().map { langs -> val availableLangs = extensionList.filterNotNull().map { langs ->
langs.map { it.lang }.distinct().toImmutableList() langs.map { it.lang }.distinct().toImmutableList()
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) }.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 { init {
scope.launch { scope.launch {
getExtensions() 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()
} }
} }
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 @Immutable
sealed class ExtensionUI { sealed class ExtensionUI {
data class Header( data class Header(

View File

@@ -119,224 +119,223 @@ private fun LibraryMap.setManga(
} }
} }
class LibraryScreenViewModel @Inject
@Inject class LibraryScreenViewModel(
constructor( private val getCategories: GetCategories,
private val getCategories: GetCategories, private val getMangaListFromCategory: GetMangaListFromCategory,
private val getMangaListFromCategory: GetMangaListFromCategory, private val removeMangaFromLibrary: RemoveMangaFromLibrary,
private val removeMangaFromLibrary: RemoveMangaFromLibrary, private val updateLibrary: UpdateLibrary,
private val updateLibrary: UpdateLibrary, private val updateCategory: UpdateCategory,
private val updateCategory: UpdateCategory, libraryPreferences: LibraryPreferences,
libraryPreferences: LibraryPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, @Assisted private val savedStateHandle: SavedStateHandle,
@Assisted private val savedStateHandle: SavedStateHandle, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf())
private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf()) val categories = library.categories.asStateFlow()
val categories = library.categories.asStateFlow()
private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 } private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 }
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
private val _showingMenu by savedStateHandle.getStateFlow { false } private val _showingMenu by savedStateHandle.getStateFlow { false }
val showingMenu = _showingMenu.asStateFlow() val showingMenu = _showingMenu.asStateFlow()
val displayMode = libraryPreferences.displayMode().stateIn(scope) val displayMode = libraryPreferences.displayMode().stateIn(scope)
val gridColumns = libraryPreferences.gridColumns().stateIn(scope) val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
val gridSize = libraryPreferences.gridSize().stateIn(scope) val gridSize = libraryPreferences.gridSize().stateIn(scope)
val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope) val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope)
val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope) val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope)
val languageBadges = libraryPreferences.languageBadge().stateIn(scope) val languageBadges = libraryPreferences.languageBadge().stateIn(scope)
val localBadges = libraryPreferences.localBadge().stateIn(scope) val localBadges = libraryPreferences.localBadge().stateIn(scope)
private val sortMode = libraryPreferences.sortMode().stateIn(scope) private val sortMode = libraryPreferences.sortMode().stateIn(scope)
private val sortAscending = libraryPreferences.sortAscending().stateIn(scope) private val sortAscending = libraryPreferences.sortAscending().stateIn(scope)
private val filter: Flow<(Manga) -> Boolean> = combine( private val filter: Flow<(Manga) -> Boolean> = combine(
libraryPreferences.filterDownloaded().getAsFlow(), libraryPreferences.filterDownloaded().getAsFlow(),
libraryPreferences.filterUnread().getAsFlow(), libraryPreferences.filterUnread().getAsFlow(),
libraryPreferences.filterCompleted().getAsFlow(), libraryPreferences.filterCompleted().getAsFlow(),
) { downloaded, unread, completed -> ) { downloaded, unread, completed ->
{ manga -> { manga ->
when (downloaded) { when (downloaded) {
FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0 FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0
FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 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 FilterState.IGNORED -> true
} && } &&
when (unread) { when (completed) {
FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0 FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED
FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0 FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED
FilterState.IGNORED -> true 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)
}
} }
.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()
}
}

View File

@@ -11,22 +11,21 @@ import ca.gosyer.jui.uicore.vm.ContextWrapper
import ca.gosyer.jui.uicore.vm.ViewModel import ca.gosyer.jui.uicore.vm.ViewModel
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class LibrarySettingsViewModel @Inject
@Inject class LibrarySettingsViewModel(
constructor( libraryPreferences: LibraryPreferences,
libraryPreferences: LibraryPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow()
val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow() val filterUnread = libraryPreferences.filterUnread().asStateFlow()
val filterUnread = libraryPreferences.filterUnread().asStateFlow() val filterCompleted = libraryPreferences.filterCompleted().asStateFlow()
val filterCompleted = libraryPreferences.filterCompleted().asStateFlow()
val sortMode = libraryPreferences.sortMode().asStateFlow() val sortMode = libraryPreferences.sortMode().asStateFlow()
val sortAscending = libraryPreferences.sortAscending().asStateFlow() val sortAscending = libraryPreferences.sortAscending().asStateFlow()
val displayMode = libraryPreferences.displayMode().asStateFlow() val displayMode = libraryPreferences.displayMode().asStateFlow()
val unreadBadges = libraryPreferences.unreadBadge().asStateFlow() val unreadBadges = libraryPreferences.unreadBadge().asStateFlow()
val downloadBadges = libraryPreferences.downloadBadge().asStateFlow() val downloadBadges = libraryPreferences.downloadBadge().asStateFlow()
val languageBadges = libraryPreferences.languageBadge().asStateFlow() val languageBadges = libraryPreferences.languageBadge().asStateFlow()
val localBadges = libraryPreferences.localBadge().asStateFlow() val localBadges = libraryPreferences.localBadge().asStateFlow()
} }

View File

@@ -14,23 +14,22 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class MainViewModel @Inject
@Inject class MainViewModel(
constructor( uiPreferences: UiPreferences,
uiPreferences: UiPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { override val scope = MainScope()
override val scope = MainScope()
val startScreen = uiPreferences.startScreen().get() val startScreen = uiPreferences.startScreen().get()
val confirmExit = uiPreferences.confirmExit().stateIn(scope) val confirmExit = uiPreferences.confirmExit().stateIn(scope)
override fun onDispose() { override fun onDispose() {
super.onDispose() super.onDispose()
scope.cancel() scope.cancel()
}
fun confirmExitToast() {
toast(MR.strings.confirm_exit_toast.toPlatformString())
}
} }
fun confirmExitToast() {
toast(MR.strings.confirm_exit_toast.toPlatformString())
}
}

View File

@@ -26,49 +26,48 @@ import kotlinx.datetime.Instant
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class AboutViewModel @Inject
@Inject class AboutViewModel(
constructor( private val dateHandler: DateHandler,
private val dateHandler: DateHandler, private val aboutServer: AboutServer,
private val aboutServer: AboutServer, private val updateChecker: UpdateChecker,
private val updateChecker: UpdateChecker, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _aboutHolder = MutableStateFlow<About?>(null)
private val _aboutHolder = MutableStateFlow<About?>(null) val aboutHolder = _aboutHolder.asStateFlow()
val aboutHolder = _aboutHolder.asStateFlow()
val formattedBuildTime = aboutHolder.map { about -> val formattedBuildTime = aboutHolder.map { about ->
about ?: return@map "" about ?: return@map ""
getFormattedDate(Instant.fromEpochSeconds(about.buildTime)) getFormattedDate(Instant.fromEpochSeconds(about.buildTime))
}.stateIn(scope, SharingStarted.Eagerly, "") }.stateIn(scope, SharingStarted.Eagerly, "")
private val _updates = MutableSharedFlow<Update.UpdateFound>() private val _updates = MutableSharedFlow<Update.UpdateFound>()
val updates = _updates.asSharedFlow() val updates = _updates.asSharedFlow()
init { init {
getAbout() getAbout()
} }
private fun getAbout() { private fun getAbout() {
scope.launch { scope.launch {
_aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) }) _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()
} }
} }
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()
}
}

View File

@@ -18,33 +18,32 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class LibraryUpdatesViewModel @Inject
@Inject class LibraryUpdatesViewModel(
constructor( private val libraryUpdateService: LibraryUpdateService,
private val libraryUpdateService: LibraryUpdateService, private val contextWrapper: ContextWrapper,
private val contextWrapper: ContextWrapper, @Assisted standalone: Boolean,
@Assisted standalone: Boolean, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val uiScope = if (standalone) {
private val uiScope = if (standalone) { MainScope()
MainScope() } else {
} else { null
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()
}
} }
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()
}
}

View File

@@ -55,386 +55,386 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class MangaScreenViewModel @Inject
@Inject class MangaScreenViewModel(
constructor( private val dateHandler: DateHandler,
private val dateHandler: DateHandler, private val getManga: GetManga,
private val getManga: GetManga, private val refreshManga: RefreshManga,
private val refreshManga: RefreshManga, private val getChapters: GetChapters,
private val getChapters: GetChapters, private val refreshChapters: RefreshChapters,
private val refreshChapters: RefreshChapters, private val updateChapter: UpdateChapter,
private val updateChapter: UpdateChapter, private val queueChapterDownload: QueueChapterDownload,
private val queueChapterDownload: QueueChapterDownload, private val stopChapterDownload: StopChapterDownload,
private val stopChapterDownload: StopChapterDownload, private val deleteChapterDownload: DeleteChapterDownload,
private val deleteChapterDownload: DeleteChapterDownload, private val getCategories: GetCategories,
private val getCategories: GetCategories, private val getMangaCategories: GetMangaCategories,
private val getMangaCategories: GetMangaCategories, private val addMangaToCategory: AddMangaToCategory,
private val addMangaToCategory: AddMangaToCategory, private val removeMangaFromCategory: RemoveMangaFromCategory,
private val removeMangaFromCategory: RemoveMangaFromCategory, private val addMangaToLibrary: AddMangaToLibrary,
private val addMangaToLibrary: AddMangaToLibrary, private val removeMangaFromLibrary: RemoveMangaFromLibrary,
private val removeMangaFromLibrary: RemoveMangaFromLibrary, private val batchChapterDownload: BatchChapterDownload,
private val batchChapterDownload: BatchChapterDownload, uiPreferences: UiPreferences,
uiPreferences: UiPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, @Assisted private val params: Params,
@Assisted private val params: Params, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _manga = MutableStateFlow<Manga?>(null)
private val _manga = MutableStateFlow<Manga?>(null) val manga = _manga.asStateFlow()
val manga = _manga.asStateFlow()
private val _chapters = MutableStateFlow<ImmutableList<ChapterDownloadItem>>(persistentListOf()) private val _chapters = MutableStateFlow<ImmutableList<ChapterDownloadItem>>(persistentListOf())
val chapters = _chapters.asStateFlow() val chapters = _chapters.asStateFlow()
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf()) private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems -> val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems ->
chapters.filter { it.isSelected(selecteditems) }.toImmutableList() chapters.filter { it.isSelected(selecteditems) }.toImmutableList()
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) }.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
private val loadingManga = MutableStateFlow(true) private val loadingManga = MutableStateFlow(true)
private val loadingChapters = MutableStateFlow(true) private val loadingChapters = MutableStateFlow(true)
private val refreshingChapters = MutableStateFlow(false) private val refreshingChapters = MutableStateFlow(false)
private val refreshingManga = MutableStateFlow(false) private val refreshingManga = MutableStateFlow(false)
val isLoading = combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d } val isLoading =
combine(loadingManga, loadingChapters, refreshingManga, refreshingChapters) { a, b, c, d -> a || b || c || d }
.stateIn(scope, SharingStarted.Eagerly, true) .stateIn(scope, SharingStarted.Eagerly, true)
val categories = getCategories.asFlow(true) val categories = getCategories.asFlow(true)
.map { it.toImmutableList() } .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 { .catch {
toast(it.message.orEmpty()) 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()) reloadChapters.onStart { emit(Unit) }.flatMapLatest {
val mangaCategories = _mangaCategories.asStateFlow() loadingChapters.value = true
getChapters.asFlow(params.mangaId)
val categoriesExist = categories.map { it.isNotEmpty() } }
.stateIn(scope, SharingStarted.Eagerly, true) .onEach {
updateChapters(it)
val inActionMode = _selectedIds.map { it.isNotEmpty() } loadingChapters.value = false
.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())) .catch {
toast(it.message.orEmpty())
log.warn(it) { "Error when getting chapters" }
loadingChapters.value = false
}
.launchIn(scope)
init { scope.launch {
DownloadService.registerWatch(params.mangaId) val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) })
.mapLatest { downloadingChapters -> if (mangaCategories != null) {
chapters.value.forEach { chapter -> _mangaCategories.value = mangaCategories.toImmutableList()
chapter.updateFrom(downloadingChapters) }
}
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) loadManga()
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) { "Error when loading manga" }
loadingManga.value = false
}
.launchIn(scope)
reloadChapters.onStart { emit(Unit) }.flatMapLatest { fun addFavorite(
loadingChapters.value = true categories: List<Category>,
getChapters.asFlow(params.mangaId) oldCategories: List<Category>,
} ) {
.onEach { scope.launch {
updateChapters(it) manga.value?.let { manga ->
loadingChapters.value = false 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 { if (categories.isEmpty()) {
toast(it.message.orEmpty()) addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) })
log.warn(it) { "Error when getting chapters" } } else {
loadingChapters.value = false categories.filterNot { it in oldCategories }.forEach {
addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) })
}
} }
.launchIn(scope)
scope.launch { val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) })
val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) })
if (mangaCategories != null) { if (mangaCategories != null) {
_mangaCategories.value = mangaCategories.toImmutableList() _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()
}
}

View File

@@ -42,7 +42,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -69,372 +68,371 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class ReaderMenuViewModel @Inject
@Inject class ReaderMenuViewModel(
constructor( private val readerPreferences: ReaderPreferences,
private val readerPreferences: ReaderPreferences, private val getManga: GetManga,
private val getManga: GetManga, private val getChapters: GetChapters,
private val getChapters: GetChapters, private val getChapter: GetChapter,
private val getChapter: GetChapter, private val getChapterPages: GetChapterPages,
private val getChapterPages: GetChapterPages, private val updateChapter: UpdateChapter,
private val updateChapter: UpdateChapter, private val updateMangaMeta: UpdateMangaMeta,
private val updateMangaMeta: UpdateMangaMeta, private val updateChapterMeta: UpdateChapterMeta,
private val updateChapterMeta: UpdateChapterMeta, private val chapterCache: ChapterCache,
private val chapterCache: ChapterCache, private val http: Http,
private val http: Http, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, @Assisted private val params: Params,
@Assisted private val params: Params, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { override val scope = MainScope()
override val scope = MainScope() private val _manga = MutableStateFlow<Manga?>(null)
private val _manga = MutableStateFlow<Manga?>(null) private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null))
private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null)) val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null)
val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null) val chapter = viewerChapters.map { it.currChapter }.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)
val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null)
private val _state = MutableStateFlow<ReaderChapter.State>(ReaderChapter.State.Wait) private val _state = MutableStateFlow<ReaderChapter.State>(ReaderChapter.State.Wait)
val state = _state.asStateFlow() val state = _state.asStateFlow()
val pages = viewerChapters.flatMapLatest { viewerChapters -> val pages = viewerChapters.flatMapLatest { viewerChapters ->
val previousChapterPages = viewerChapters.prevChapter val previousChapterPages = viewerChapters.prevChapter
?.pages ?.pages
?.map { (it as? PagesState.Success)?.pages } ?.map { (it as? PagesState.Success)?.pages }
?: flowOf(null) ?: flowOf(null)
val chapterPages = viewerChapters.currChapter val chapterPages = viewerChapters.currChapter
?.pages ?.pages
?.map { (it as? PagesState.Success)?.pages } ?.map { (it as? PagesState.Success)?.pages }
?: flowOf(null) ?: flowOf(null)
val nextChapterPages = viewerChapters.nextChapter val nextChapterPages = viewerChapters.nextChapter
?.pages ?.pages
?.map { (it as? PagesState.Success)?.pages } ?.map { (it as? PagesState.Success)?.pages }
?: flowOf(null) ?: flowOf(null)
combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next -> combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next ->
( (
prev.orEmpty() + prev.orEmpty() +
ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) + ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) +
cur.orEmpty() + cur.orEmpty() +
ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) + ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) +
next.orEmpty() next.orEmpty()
).toImmutableList() ).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() { private val _currentPage = MutableStateFlow<ReaderItem?>(null)
scope.launchDefault { val currentPage = _currentPage.asStateFlow()
runCatching {
initManga(params.mangaId) private val _currentPageOffset = MutableStateFlow(1)
initChapters(params.mangaId, params.chapterId) 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 { val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode)
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 { private val loader = ChapterLoader(
scope.launch { readerPreferences = readerPreferences,
val moveTo = when (navigationRegion) { http = http,
Navigation.MENU -> { chapterCache = chapterCache,
setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value) bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper),
null getChapterPages = getChapterPages,
} )
Navigation.NEXT -> MoveTo.Next init {
init()
}
Navigation.PREV -> MoveTo.Previous fun init() {
scope.launchDefault {
Navigation.RIGHT -> when (readerModeSettings.direction.value) { runCatching {
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) 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()
}
}

View File

@@ -73,47 +73,46 @@ class SettingsAdvancedScreen : Screen {
} }
} }
class SettingsAdvancedViewModel @Inject
@Inject class SettingsAdvancedViewModel(
constructor( updatePreferences: UpdatePreferences,
updatePreferences: UpdatePreferences, private val imageCache: ImageCache,
private val imageCache: ImageCache, private val chapterCache: ChapterCache,
private val chapterCache: ChapterCache, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val updatesEnabled = updatePreferences.enabled().asStateFlow()
val updatesEnabled = updatePreferences.enabled().asStateFlow()
val imageCacheSize = flow { val imageCacheSize = flow {
while (currentCoroutineContext().isActive) { while (currentCoroutineContext().isActive) {
emit(imageCache.size.bytesIntoHumanReadable()) emit(imageCache.size.bytesIntoHumanReadable())
delay(1.seconds) 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()
}
} }
}.stateIn(scope, SharingStarted.Eagerly, "")
fun clearChapterCache() { val chapterCacheSize = flow {
scope.launchIO { while (currentCoroutineContext().isActive) {
chapterCache.clear() emit(chapterCache.size.bytesIntoHumanReadable())
} delay(1.seconds)
} }
}.stateIn(scope, SharingStarted.Eagerly, "")
companion object { fun clearImageCache() {
private val log = logging() scope.launchIO {
imageCache.clear()
} }
} }
fun clearChapterCache() {
scope.launchIO {
chapterCache.clear()
}
}
companion object {
private val log = logging()
}
}
@Composable @Composable
fun SettingsAdvancedScreenContent( fun SettingsAdvancedScreenContent(
updatesEnabled: PreferenceMutableStateFlow<Boolean>, updatesEnabled: PreferenceMutableStateFlow<Boolean>,

View File

@@ -108,23 +108,22 @@ class SettingsAppearanceScreen : Screen {
} }
} }
class ThemesViewModel @Inject
@Inject class ThemesViewModel(
constructor( private val uiPreferences: UiPreferences,
private val uiPreferences: UiPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val themeMode = uiPreferences.themeMode().asStateFlow()
val themeMode = uiPreferences.themeMode().asStateFlow() val lightTheme = uiPreferences.lightTheme().asStateFlow()
val lightTheme = uiPreferences.lightTheme().asStateFlow() val darkTheme = uiPreferences.darkTheme().asStateFlow()
val darkTheme = uiPreferences.darkTheme().asStateFlow() val lightColors = uiPreferences.getLightColors().asStateFlow(scope)
val lightColors = uiPreferences.getLightColors().asStateFlow(scope) val darkColors = uiPreferences.getDarkColors().asStateFlow(scope)
val darkColors = uiPreferences.getDarkColors().asStateFlow(scope)
val windowDecorations = uiPreferences.windowDecorations().asStateFlow() val windowDecorations = uiPreferences.windowDecorations().asStateFlow()
@Composable @Composable
fun getActiveColors(): AppColorsPreferenceState = if (MaterialTheme.colors.isLight) lightColors else darkColors fun getActiveColors(): AppColorsPreferenceState = if (MaterialTheme.colors.isLight) lightColors else darkColors
} }
expect val showWindowDecorationsOption: Boolean expect val showWindowDecorationsOption: Boolean

View File

@@ -118,157 +118,156 @@ class SettingsBackupScreen : Screen {
} }
} }
class SettingsBackupViewModel @Inject
@Inject class SettingsBackupViewModel(
constructor( private val validateBackupFile: ValidateBackupFile,
private val validateBackupFile: ValidateBackupFile, private val importBackupFile: ImportBackupFile,
private val importBackupFile: ImportBackupFile, private val exportBackupFile: ExportBackupFile,
private val exportBackupFile: ExportBackupFile, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _restoreStatus = MutableStateFlow<Status>(Status.Nothing)
private val _restoreStatus = MutableStateFlow<Status>(Status.Nothing) val restoreStatus = _restoreStatus.asStateFlow()
val restoreStatus = _restoreStatus.asStateFlow()
private val _missingSourceFlow = MutableSharedFlow<Pair<Path, ImmutableList<String>>>() private val _missingSourceFlow = MutableSharedFlow<Pair<Path, ImmutableList<String>>>()
val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow()) val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow())
private val _creatingStatus = MutableStateFlow<Status>(Status.Nothing) private val _creatingStatus = MutableStateFlow<Status>(Status.Nothing)
val creatingStatus = _creatingStatus.asStateFlow() val creatingStatus = _creatingStatus.asStateFlow()
private val _createFlow = MutableSharedFlow<String>() private val _createFlow = MutableSharedFlow<String>()
val createFlowHolder = StableHolder(_createFlow.asSharedFlow()) val createFlowHolder = StableHolder(_createFlow.asSharedFlow())
fun restoreFile(source: Source) { fun restoreFile(source: Source) {
scope.launch { scope.launch {
val file = try { val file = try {
FileSystem.SYSTEM_TEMPORARY_DIRECTORY FileSystem.SYSTEM_TEMPORARY_DIRECTORY
.resolve("tachidesk.${Random.nextLong()}.tachibk") .resolve("tachidesk.${Random.nextLong()}.tachibk")
.also { file -> .also { file ->
source.saveTo(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())
}
} }
.catch { } catch (e: Exception) {
toast(it.message.orEmpty()) log.warn(e) { "Error creating backup file" }
log.warn(it) { "Error importing backup" } _restoreStatus.value = Status.Error
_restoreStatus.value = Status.Error e.throwIfCancellation()
} return@launch
.collect()
} }
}
fun restoreBackup(file: Path) { validateBackupFile.asFlow(file)
importBackupFile .onEach { (missingSources) ->
.asFlow(file) if (missingSources.isEmpty()) {
.onStart { restoreBackup(file)
_restoreStatus.value = Status.InProgress(null) } else {
} _missingSourceFlow.emit(file to missingSources.toImmutableList())
.onEach { }
_restoreStatus.value = it.second.toStatus()
} }
.catch { .catch {
toast(it.message.orEmpty()) toast(it.message.orEmpty())
log.warn(it) { "Error importing backup" } log.warn(it) { "Error importing backup" }
_restoreStatus.value = Status.Error _restoreStatus.value = Status.Error
} }
.launchIn(scope) .collect()
} }
}
private fun RestoreStatus.toStatus() = when (state) { fun restoreBackup(file: Path) {
RestoreState.IDLE -> Status.Success importBackupFile
RestoreState.SUCCESS -> Status.Success .asFlow(file)
RestoreState.FAILURE -> Status.Error .onStart {
RestoreState.RESTORING_CATEGORIES -> Status.InProgress(0.01f) _restoreStatus.value = Status.InProgress(null)
RestoreState.RESTORING_SETTINGS -> Status.InProgress(0.02f) }
RestoreState.RESTORING_MANGA -> Status.InProgress((completed.toFloat() / total).coerceIn(0f, 0.99f)) .onEach {
RestoreState.RESTORING_META -> Status.InProgress(1f) _restoreStatus.value = it.second.toStatus()
RestoreState.UNKNOWN -> Status.Error }
} .catch {
toast(it.message.orEmpty())
log.warn(it) { "Error importing backup" }
_restoreStatus.value = Status.Error
}
.launchIn(scope)
}
fun stopRestore() { private fun RestoreStatus.toStatus() = when (state) {
_restoreStatus.value = Status.Error 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) fun stopRestore() {
private val mutex = Mutex() _restoreStatus.value = Status.Error
}
fun exportBackup() { private val tempFile = MutableStateFlow<Path?>(null)
exportBackupFile private val mutex = Mutex()
.asFlow(
true, true // todo fun exportBackup() {
) { exportBackupFile
onDownload { bytesSentTotal, contentLength -> .asFlow(
_creatingStatus.value = Status.InProgress( true, true, // todo
(bytesSentTotal.toFloat() / contentLength) ) {
.coerceAtMost(0.99F), onDownload { bytesSentTotal, contentLength ->
) _creatingStatus.value = Status.InProgress(
} (bytesSentTotal.toFloat() / contentLength)
.coerceAtMost(0.99F),
)
} }
.onStart { }
_creatingStatus.value = Status.InProgress(null) .onStart {
} _creatingStatus.value = Status.InProgress(null)
.onEach { (filename, source) -> }
tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { .onEach { (filename, source) ->
mutex.tryLock() tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also {
scope.launch { mutex.tryLock()
try { scope.launch {
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) {
try { try {
FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer()) source.saveTo(it)
_creatingStatus.value = Status.Success
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
log.error(e) { "Error moving created backup" } log.warn(e) { "Error creating backup" }
_creatingStatus.value = Status.Error _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 _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 { sealed class Status {
data object Nothing : Status() data object Nothing : Status()

View File

@@ -78,63 +78,67 @@ class SettingsGeneralScreen : Screen {
} }
} }
class SettingsGeneralViewModel @Inject
@Inject class SettingsGeneralViewModel(
constructor( private val dateHandler: DateHandler,
private val dateHandler: DateHandler, uiPreferences: UiPreferences,
uiPreferences: UiPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val startScreen = uiPreferences.startScreen().asStateFlow()
val startScreen = uiPreferences.startScreen().asStateFlow() val confirmExit = uiPreferences.confirmExit().asStateFlow()
val confirmExit = uiPreferences.confirmExit().asStateFlow() val language = uiPreferences.language().asStateFlow()
val language = uiPreferences.language().asStateFlow() val dateFormat = uiPreferences.dateFormat().asStateFlow()
val dateFormat = uiPreferences.dateFormat().asStateFlow()
private val now = Clock.System.now() private val now = Clock.System.now()
private val currentLocale = Locale.current private val currentLocale = Locale.current
@Composable @Composable
fun getStartScreenChoices(): ImmutableMap<StartScreen, String> = fun getStartScreenChoices(): ImmutableMap<StartScreen, String> =
persistentMapOf( persistentMapOf(
StartScreen.Library to stringResource(MR.strings.location_library), StartScreen.Library to stringResource(MR.strings.location_library),
StartScreen.Updates to stringResource(MR.strings.location_updates), StartScreen.Updates to stringResource(MR.strings.location_updates),
StartScreen.Sources to stringResource(MR.strings.location_sources), StartScreen.Sources to stringResource(MR.strings.location_sources),
StartScreen.Extensions to stringResource(MR.strings.location_extensions), StartScreen.Extensions to stringResource(MR.strings.location_extensions),
) )
@Composable @Composable
fun getLanguageChoices(): ImmutableMap<String, String> { fun getLanguageChoices(): ImmutableMap<String, String> {
val langJsonState = MR.files.languages_json.readTextAsync() val langJsonState = MR.files.languages_json.readTextAsync()
val langs by produceState(emptyMap(), langJsonState.value) { val langs by produceState(emptyMap(), langJsonState.value) {
val langJson = langJsonState.value val langJson = langJsonState.value
if (langJson != null) { if (langJson != null) {
withIOContext { withIOContext {
value = Json.decodeFromString<JsonObject>(langJson)["langs"] value = Json.decodeFromString<JsonObject>(langJson)["langs"]
?.jsonArray ?.jsonArray
.orEmpty() .orEmpty()
.map { it.jsonPrimitive.content } .map { it.jsonPrimitive.content }
.associateWith { Locale(it).getDisplayName(currentLocale) } .associateWith { Locale(it).getDisplayName(currentLocale) }
}
} }
} }
return mapOf("" to stringResource(MR.strings.language_system_default, currentLocale.getDisplayName(currentLocale)))
.plus(langs)
.toImmutableMap()
} }
return mapOf(
@Composable "" to stringResource(
fun getDateChoices(): ImmutableMap<String, String> = MR.strings.language_system_default,
dateHandler.formatOptions currentLocale.getDisplayName(currentLocale),
.associateWith { ),
it.ifEmpty { stringResource(MR.strings.date_system_default) } + )
" (${getFormattedDate(it)})" .plus(langs)
} .toImmutableMap()
.toImmutableMap()
@Composable
private fun getFormattedDate(prefValue: String): String = dateHandler.getDateFormat(prefValue).invoke(now)
} }
@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 @Composable
fun SettingsGeneralScreenContent( fun SettingsGeneralScreenContent(
startScreen: PreferenceMutableStateFlow<StartScreen>, startScreen: PreferenceMutableStateFlow<StartScreen>,

View File

@@ -94,38 +94,37 @@ class SettingsLibraryScreen : Screen {
} }
} }
class SettingsLibraryViewModel @Inject
@Inject class SettingsLibraryViewModel(
constructor( libraryPreferences: LibraryPreferences,
libraryPreferences: LibraryPreferences, private val getCategories: GetCategories,
private val getCategories: GetCategories, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val displayMode = libraryPreferences.displayMode().asStateFlow()
val displayMode = libraryPreferences.displayMode().asStateFlow() val gridColumns = libraryPreferences.gridColumns().asStateFlow()
val gridColumns = libraryPreferences.gridColumns().asStateFlow() val gridSize = libraryPreferences.gridSize().asStateFlow()
val gridSize = libraryPreferences.gridSize().asStateFlow()
val showAllCategory = libraryPreferences.showAllCategory().asStateFlow() val showAllCategory = libraryPreferences.showAllCategory().asStateFlow()
private val _categories = MutableStateFlow(0) private val _categories = MutableStateFlow(0)
val categories = _categories.asStateFlow() val categories = _categories.asStateFlow()
init { init {
refreshCategoryCount() 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()
} }
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 @Composable
fun SettingsLibraryScreenContent( fun SettingsLibraryScreenContent(
displayMode: PreferenceMutableStateFlow<DisplayMode>, displayMode: PreferenceMutableStateFlow<DisplayMode>,

View File

@@ -88,75 +88,75 @@ class SettingsReaderScreen : Screen {
} }
} }
class SettingsReaderViewModel @Inject
@Inject class SettingsReaderViewModel(
constructor( readerPreferences: ReaderPreferences,
readerPreferences: ReaderPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val modes = readerPreferences.modes().asStateFlow()
val modes = readerPreferences.modes().asStateFlow() .map {
.map { it.associateWith { it }
it.associateWith { it } .toImmutableMap()
.toImmutableMap() }
} .stateIn(scope, SharingStarted.Eagerly, persistentMapOf())
.stateIn(scope, SharingStarted.Eagerly, persistentMapOf()) val selectedMode = readerPreferences.mode().asStateIn(scope)
val selectedMode = readerPreferences.mode().asStateIn(scope)
private val _modeSettings = MutableStateFlow<ImmutableList<StableHolder<ReaderModePreference>>>( private val _modeSettings = MutableStateFlow<ImmutableList<StableHolder<ReaderModePreference>>>(
persistentListOf(), persistentListOf(),
) )
val modeSettings = _modeSettings.asStateFlow() val modeSettings = _modeSettings.asStateFlow()
init { init {
modes.onEach { modes -> modes.onEach { modes ->
val modeSettings = _modeSettings.value val modeSettings = _modeSettings.value
val modesInSettings = modeSettings.map { it.item.mode } val modesInSettings = modeSettings.map { it.item.mode }
_modeSettings.value = modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) -> _modeSettings.value =
modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) ->
it !in modesInSettings it !in modesInSettings
}.map { (it) -> }.map { (it) ->
StableHolder(ReaderModePreference(scope, it, readerPreferences.getMode(it))) StableHolder(ReaderModePreference(scope, it, readerPreferences.getMode(it)))
} }
}.launchIn(scope) }.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()
} }
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( data class ReaderModePreference(
val scope: CoroutineScope, val scope: CoroutineScope,
val mode: String, val mode: String,

View File

@@ -191,7 +191,8 @@ class ServerSettings(
getSetting = { it.backupTime }, getSetting = { it.backupTime },
getInput = { SetSettingsInput(backupTime = it) }, getInput = { SetSettingsInput(backupTime = it) },
) )
// val basicAuthEnabled = getServerFlow(
// val basicAuthEnabled = getServerFlow(
// getSetting = { it.basicAuthEnabled }, // getSetting = { it.basicAuthEnabled },
// getInput = { SetSettingsInput(basicAuthEnabled = it) }, // getInput = { SetSettingsInput(basicAuthEnabled = it) },
// ) // )
@@ -263,7 +264,8 @@ class ServerSettings(
getSetting = { it.globalUpdateInterval.toString() }, getSetting = { it.globalUpdateInterval.toString() },
getInput = { SetSettingsInput(globalUpdateInterval = it.toDoubleOrNull()?.takeIf { it !in 0.01..5.99 }) }, getInput = { SetSettingsInput(globalUpdateInterval = it.toDoubleOrNull()?.takeIf { it !in 0.01..5.99 }) },
) )
// val gqlDebugLogsEnabled = getServerFlow(
// val gqlDebugLogsEnabled = getServerFlow(
// getSetting = { it.gqlDebugLogsEnabled }, // getSetting = { it.gqlDebugLogsEnabled },
// getInput = { SetSettingsInput(gqlDebugLogsEnabled = it) }, // getInput = { SetSettingsInput(gqlDebugLogsEnabled = it) },
// ) // )
@@ -360,67 +362,66 @@ class ServerSettings(
) )
} }
class SettingsServerViewModel @Inject
@Inject class SettingsServerViewModel(
constructor( private val getSettings: GetSettings,
private val getSettings: GetSettings, private val setSettings: SetSettings,
private val setSettings: SetSettings, serverPreferences: ServerPreferences,
serverPreferences: ServerPreferences, serverHostPreferences: ServerHostPreferences,
serverHostPreferences: ServerHostPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val serverUrl = serverPreferences.server().asStateIn(scope)
val serverUrl = serverPreferences.server().asStateIn(scope) val serverPort = serverPreferences.port().asStringStateIn(scope)
val serverPort = serverPreferences.port().asStringStateIn(scope) val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(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 @Composable
fun getProxyChoices(): ImmutableMap<Proxy, String> = fun getProxyChoices(): ImmutableMap<Proxy, String> =
persistentMapOf( persistentMapOf(
Proxy.NO_PROXY to stringResource(MR.strings.no_proxy), Proxy.NO_PROXY to stringResource(MR.strings.no_proxy),
Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy), Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy),
Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy), Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy),
) )
val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope) val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope)
val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope) val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope)
val socksHost = serverPreferences.proxySocksHost().asStateIn(scope) val socksHost = serverPreferences.proxySocksHost().asStateIn(scope)
val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope) val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope)
val auth = serverPreferences.auth().asStateIn(scope) val auth = serverPreferences.auth().asStateIn(scope)
@Composable @Composable
fun getAuthChoices(): ImmutableMap<Auth, String> = fun getAuthChoices(): ImmutableMap<Auth, String> =
persistentMapOf( persistentMapOf(
Auth.NONE to stringResource(MR.strings.no_auth), Auth.NONE to stringResource(MR.strings.no_auth),
Auth.BASIC to stringResource(MR.strings.basic_auth), Auth.BASIC to stringResource(MR.strings.basic_auth),
Auth.DIGEST to stringResource(MR.strings.digest_auth), Auth.DIGEST to stringResource(MR.strings.digest_auth),
) )
val authUsername = serverPreferences.authUsername().asStateIn(scope) val authUsername = serverPreferences.authUsername().asStateIn(scope)
val authPassword = serverPreferences.authPassword().asStateIn(scope) val authPassword = serverPreferences.authPassword().asStateIn(scope)
private val _serverSettings = MutableStateFlow<ServerSettings?>(null) private val _serverSettings = MutableStateFlow<ServerSettings?>(null)
val serverSettings = _serverSettings.asStateFlow() val serverSettings = _serverSettings.asStateFlow()
init { init {
scope.launchIO { scope.launchIO {
val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) }) val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) })
if (initialSettings != null) { if (initialSettings != null) {
_serverSettings.value = ServerSettings( _serverSettings.value = ServerSettings(
getSettings, getSettings,
setSettings, setSettings,
scope, scope,
initialSettings, initialSettings,
onError = { toast(it) }, onError = { toast(it) },
) )
}
} }
} }
} }
}
@Composable @Composable
fun SettingsServerScreenContent( fun SettingsServerScreenContent(
@@ -886,7 +887,7 @@ private val repoRegex =
( (
"https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" + "https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" +
"\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?" "\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?"
).toRegex() ).toRegex()
@Composable @Composable
fun ExtensionReposDialog( fun ExtensionReposDialog(

View File

@@ -43,129 +43,128 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class GlobalSearchViewModel @Inject
@Inject class GlobalSearchViewModel(
constructor( private val getSourceList: GetSourceList,
private val getSourceList: GetSourceList, private val getSearchManga: GetSearchManga,
private val getSearchManga: GetSearchManga, catalogPreferences: CatalogPreferences,
catalogPreferences: CatalogPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, @Assisted private val savedStateHandle: SavedStateHandle,
@Assisted private val savedStateHandle: SavedStateHandle, @Assisted params: Params,
@Assisted params: Params, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _query by savedStateHandle.getStateFlow { params.initialQuery }
private val _query by savedStateHandle.getStateFlow { params.initialQuery } val query = _query.asStateFlow()
val query = _query.asStateFlow()
private val installedSources = MutableStateFlow(emptyList<Source>()) private val installedSources = MutableStateFlow(emptyList<Source>())
private val languages = catalogPreferences.languages().stateIn(scope) private val languages = catalogPreferences.languages().stateIn(scope)
val displayMode = catalogPreferences.displayMode().stateIn(scope) val displayMode = catalogPreferences.displayMode().stateIn(scope)
private val _isLoading = MutableStateFlow(true) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
val sources = combine(installedSources, languages) { installedSources, languages -> val sources = combine(installedSources, languages) { installedSources, languages ->
installedSources.filter { installedSources.filter {
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
}.toImmutableList() }.toImmutableList()
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) }.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 { init {
getSources() getSources()
readySearch() readySearch()
} }
private fun getSources() { private fun getSources() {
getSourceList.asFlow() getSourceList.asFlow()
.onEach { sources -> .onEach { sources ->
installedSources.value = sources.sortedWith( installedSources.value = sources.sortedWith(
compareBy<Source, String>(String.CASE_INSENSITIVE_ORDER) { it.lang } compareBy<Source, String>(String.CASE_INSENSITIVE_ORDER) { it.lang }
.thenBy(String.CASE_INSENSITIVE_ORDER) { .thenBy(String.CASE_INSENSITIVE_ORDER) {
it.name it.name
}, },
) )
_isLoading.value = false _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)
} }
} .catch {
toast(it.message.orEmpty())
log.warn(it) { "Error getting sources" }
_isLoading.value = false
}
.launchIn(scope)
}
private companion object { private val semaphore = Semaphore(5)
private val log = logging()
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()
}
}

View File

@@ -34,100 +34,99 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class SourceHomeScreenViewModel @Inject
@Inject class SourceHomeScreenViewModel(
constructor( private val getSourceList: GetSourceList,
private val getSourceList: GetSourceList, catalogPreferences: CatalogPreferences,
catalogPreferences: CatalogPreferences, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, @Assisted private val savedStateHandle: SavedStateHandle,
@Assisted private val savedStateHandle: SavedStateHandle, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _isLoading = MutableStateFlow(true)
private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow()
val isLoading = _isLoading.asStateFlow()
private val installedSources = MutableStateFlow(emptyList<Source>()) private val installedSources = MutableStateFlow(emptyList<Source>())
private val _languages = catalogPreferences.languages().asStateFlow() private val _languages = catalogPreferences.languages().asStateFlow()
val languages = _languages.asStateFlow() val languages = _languages.asStateFlow()
.map { it.toImmutableSet() } .map { it.toImmutableSet() }
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) .stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
val sources = combine(installedSources, languages) { installedSources, languages -> val sources = combine(installedSources, languages) { installedSources, languages ->
val all = MR.strings.all.toPlatformString() val all = MR.strings.all.toPlatformString()
val other = MR.strings.other.toPlatformString() val other = MR.strings.other.toPlatformString()
installedSources installedSources
.distinctBy { it.id } .distinctBy { it.id }
.filter { .filter {
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID 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 { .toList()
it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name)) .sortedWith(
.map(SourceUI::SourceItem) compareBy<Pair<String, *>> { (key) ->
}
.mapKeys { (key) ->
when (key) { when (key) {
"all" -> all all -> 1
"other" -> other other -> 3
else -> Locale(key).displayName else -> 2
} }
} }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair<String, *>::first),
.toList() )
.sortedWith( .flatMap { (key, value) ->
compareBy<Pair<String, *>> { (key) -> listOf(SourceUI.Header(key)) + value
when (key) { }
all -> 1 .toImmutableList()
other -> 3 }.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
else -> 2
}
}.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 -> val sourceLanguages = installedSources.map { sources ->
sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG) sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG)
.toImmutableList() .toImmutableList()
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) }.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
private val _query by savedStateHandle.getStateFlow { "" } private val _query by savedStateHandle.getStateFlow { "" }
val query = _query.asStateFlow() val query = _query.asStateFlow()
init { init {
getSources() 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()
}
} }
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 @Stable
sealed class SourceUI { sealed class SourceUI {
@Stable @Stable

View File

@@ -28,65 +28,64 @@ import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class SourceSettingsScreenViewModel @Inject
@Inject class SourceSettingsScreenViewModel(
constructor( private val getSourceSettings: GetSourceSettings,
private val getSourceSettings: GetSourceSettings, private val setSourceSetting: SetSourceSetting,
private val setSourceSetting: SetSourceSetting, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, @Assisted private val params: Params,
@Assisted private val params: Params, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _loading = MutableStateFlow(true)
private val _loading = MutableStateFlow(true) val loading = _loading.asStateFlow()
val loading = _loading.asStateFlow()
private val _sourceSettings = MutableStateFlow<ImmutableList<SourceSettingsView<*, *>>>(persistentListOf()) private val _sourceSettings = MutableStateFlow<ImmutableList<SourceSettingsView<*, *>>>(persistentListOf())
val sourceSettings = _sourceSettings.asStateFlow() val sourceSettings = _sourceSettings.asStateFlow()
init { init {
getSourceSettings() getSourceSettings()
sourceSettings.mapLatest { settings -> sourceSettings.mapLatest { settings ->
supervisorScope { supervisorScope {
settings.forEach { setting -> settings.forEach { setting ->
setting.state.drop(1) setting.state.drop(1)
.filterNotNull() .filterNotNull()
.onEach { .onEach {
setSourceSetting.await( setSourceSetting.await(
sourceId = params.sourceId, sourceId = params.sourceId,
setting.props, setting.props,
onError = { toast(it.message.orEmpty()) }, onError = { toast(it.message.orEmpty()) },
) )
getSourceSettings() getSourceSettings()
} }
.launchIn(this) .launchIn(this)
}
} }
}.launchIn(scope) }
} }.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()
}
} }
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()
}
}

View File

@@ -40,204 +40,203 @@ import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
class UpdatesScreenViewModel @Inject
@Inject class UpdatesScreenViewModel(
constructor( private val queueChapterDownload: QueueChapterDownload,
private val queueChapterDownload: QueueChapterDownload, private val stopChapterDownload: StopChapterDownload,
private val stopChapterDownload: StopChapterDownload, private val deleteChapterDownload: DeleteChapterDownload,
private val deleteChapterDownload: DeleteChapterDownload, private val getRecentUpdates: GetRecentUpdates,
private val getRecentUpdates: GetRecentUpdates, private val updateChapter: UpdateChapter,
private val updateChapter: UpdateChapter, private val batchChapterDownload: BatchChapterDownload,
private val batchChapterDownload: BatchChapterDownload, private val updateLibrary: UpdateLibrary,
private val updateLibrary: UpdateLibrary, private val updatesPager: UpdatesPager,
private val updatesPager: UpdatesPager, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { private val _isLoading = MutableStateFlow(true)
private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow()
val isLoading = _isLoading.asStateFlow()
val updates = updatesPager.updates.map { updates -> val updates = updatesPager.updates.map { updates ->
updates.map { updates.map {
when (it) { when (it) {
is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date) is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date)
is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter)) 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 setRead(
chapterIds: List<Long>,
private fun setBookmarked( read: Boolean,
chapterIds: List<Long>, ) {
bookmark: Boolean, scope.launch {
) { updateChapter.await(chapterIds, read = read, onError = { toast(it.message.orEmpty()) })
scope.launch { _selectedIds.value = persistentListOf()
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()
} }
} }
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 { sealed class UpdatesUI {
data class Item( data class Item(
val chapterDownloadItem: ChapterDownloadItem, val chapterDownloadItem: ChapterDownloadItem,

View File

@@ -18,26 +18,26 @@ import kotlinx.coroutines.flow.shareIn
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging import org.lighthousegames.logging.logging
@Inject
class TrayViewModel class TrayViewModel
@Inject constructor(
constructor( updateChecker: UpdateChecker,
updateChecker: UpdateChecker, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { override val scope = MainScope()
override val scope = MainScope()
val updateFound = updateChecker val updateFound = updateChecker
.asFlow(false) .asFlow(false)
.catch { log.warn(it) { "Failed to check for updates" } } .catch { log.warn(it) { "Failed to check for updates" } }
.filterIsInstance<UpdateChecker.Update.UpdateFound>() .filterIsInstance<UpdateChecker.Update.UpdateFound>()
.shareIn(scope, SharingStarted.Eagerly, 1) .shareIn(scope, SharingStarted.Eagerly, 1)
override fun onDispose() { override fun onDispose() {
super.onDispose() super.onDispose()
scope.cancel() scope.cancel()
}
companion object {
private val log = logging()
}
} }
companion object {
private val log = logging()
}
}

View File

@@ -66,72 +66,72 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie
} }
} }
@Inject
actual class SettingsServerHostViewModel actual class SettingsServerHostViewModel
@Inject constructor(
constructor( serverPreferences: ServerPreferences,
serverPreferences: ServerPreferences, serverHostPreferences: ServerHostPreferences,
serverHostPreferences: ServerHostPreferences, private val serverService: ServerService,
private val serverService: ServerService, contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { val host = serverHostPreferences.host().asStateIn(scope)
val host = serverHostPreferences.host().asStateIn(scope)
// IP // IP
val ip = serverHostPreferences.ip().asStateIn(scope) val ip = serverHostPreferences.ip().asStateIn(scope)
val port = serverHostPreferences.port().asStringStateIn(scope) val port = serverHostPreferences.port().asStringStateIn(scope)
// Root // Root
val rootPath = serverHostPreferences.rootPath().asStateIn(scope) val rootPath = serverHostPreferences.rootPath().asStateIn(scope)
// Downloader // Downloader
val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope)
// Backup // Backup
val backupPath = serverHostPreferences.backupPath().asStateIn(scope) val backupPath = serverHostPreferences.backupPath().asStateIn(scope)
// LocalSource // LocalSource
val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope) val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope)
// Authentication // Authentication
val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope)
val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope)
val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope)
private val _serverSettingChanged = MutableStateFlow(false) private val _serverSettingChanged = MutableStateFlow(false)
val serverSettingChanged = _serverSettingChanged.asStateFlow() val serverSettingChanged = _serverSettingChanged.asStateFlow()
fun serverSettingChanged() { fun serverSettingChanged() {
_serverSettingChanged.value = true _serverSettingChanged.value = true
} }
fun restartServer() { fun restartServer() {
if (serverSettingChanged.value) { if (serverSettingChanged.value) {
serverService.startServer() 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)
} }
} }
// 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( fun LazyListScope.ServerHostItems(
hostValue: Boolean, hostValue: Boolean,
basicAuthEnabledValue: Boolean, basicAuthEnabledValue: Boolean,

View File

@@ -11,12 +11,12 @@ import ca.gosyer.jui.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@Inject
actual class DebugOverlayViewModel actual class DebugOverlayViewModel
@Inject constructor(
constructor( contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { actual val maxMemory: String
actual val maxMemory: String get() = ""
get() = "" actual val usedMemoryFlow: MutableStateFlow<String> = MutableStateFlow("")
actual val usedMemoryFlow: MutableStateFlow<String> = MutableStateFlow("") }
}

View File

@@ -15,8 +15,8 @@ import me.tatarka.inject.annotations.Inject
@Composable @Composable
actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {} actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit = {}
@Inject
actual class SettingsServerHostViewModel actual class SettingsServerHostViewModel
@Inject constructor(
constructor( contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper)
) : ViewModel(contextWrapper)

View File

@@ -16,36 +16,36 @@ import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@Inject
actual class DebugOverlayViewModel actual class DebugOverlayViewModel
@Inject constructor(
constructor( contextWrapper: ContextWrapper,
contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) {
) : ViewModel(contextWrapper) { override val scope = MainScope()
override val scope = MainScope()
val runtime: Runtime = Runtime.getRuntime() val runtime: Runtime = Runtime.getRuntime()
actual val maxMemory = runtime.maxMemory().formatSize() actual val maxMemory = runtime.maxMemory().formatSize()
actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize())
init { init {
scope.launch { scope.launch {
while (true) { while (true) {
usedMemoryFlow.value = runtime.usedMemory().formatSize() usedMemoryFlow.value = runtime.usedMemory().formatSize()
delay(100.milliseconds) 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()
}
}