Integrate Ktorfit

This commit is contained in:
Syer10
2022-09-22 22:30:24 -04:00
parent 0057de2e25
commit 7a7e92a7b4
61 changed files with 798 additions and 1455 deletions

View File

@@ -62,6 +62,10 @@ dependencies {
implementation(libs.ktor.websockets)
implementation(libs.ktor.auth)
// Ktorfit
implementation(libs.ktorfit.lib)
ksp(libs.ktorfit.ksp)
// Logging
implementation(libs.logging.kmlogging)

View File

@@ -26,7 +26,6 @@ import ca.gosyer.jui.domain.download.model.DownloadState
import ca.gosyer.jui.domain.download.model.DownloadStatus
import ca.gosyer.jui.domain.download.service.DownloadService
import ca.gosyer.jui.domain.download.service.DownloadService.Companion.status
import ca.gosyer.jui.domain.server.model.requests.downloadsQuery
import ca.gosyer.jui.i18n.MR
import dev.icerock.moko.resources.desc.desc
import dev.icerock.moko.resources.format
@@ -146,7 +145,7 @@ class AndroidDownloadService : Service() {
client.ws(
host = serverUrl.host,
port = serverUrl.port,
path = serverUrl.encodedPath + downloadsQuery()
path = serverUrl.encodedPath + "/api/v1/downloads"
) {
errorConnectionCount = 0
status.value = Status.RUNNING

View File

@@ -17,3 +17,10 @@ fun String.chop(count: Int, replacement: String = "…"): String {
this
}
}
fun String.addSuffix(char: Char): String {
return if (endsWith(char)) {
this
} else this + char
}

View File

@@ -6,57 +6,60 @@
package ca.gosyer.jui.data
import ca.gosyer.jui.data.backup.BackupRepositoryImpl
import ca.gosyer.jui.data.category.CategoryRepositoryImpl
import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl
import ca.gosyer.jui.data.download.DownloadRepositoryImpl
import ca.gosyer.jui.data.extension.ExtensionRepositoryImpl
import ca.gosyer.jui.data.library.LibraryRepositoryImpl
import ca.gosyer.jui.data.manga.MangaRepositoryImpl
import ca.gosyer.jui.data.settings.SettingsRepositoryImpl
import ca.gosyer.jui.data.source.SourceRepositoryImpl
import ca.gosyer.jui.data.updates.UpdatesRepositoryImpl
import ca.gosyer.jui.core.lang.addSuffix
import ca.gosyer.jui.domain.backup.service.BackupRepository
import ca.gosyer.jui.domain.category.service.CategoryRepository
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.createIt
import ca.gosyer.jui.domain.download.service.DownloadRepository
import ca.gosyer.jui.domain.extension.service.ExtensionRepository
import ca.gosyer.jui.domain.library.service.LibraryRepository
import ca.gosyer.jui.domain.manga.service.MangaRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.service.ServerPreferences
import ca.gosyer.jui.domain.settings.service.SettingsRepository
import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.domain.updates.service.UpdatesRepository
import de.jensklingenberg.ktorfit.Ktorfit
import me.tatarka.inject.annotations.Provides
interface DataComponent {
val BackupRepositoryImpl.bind: BackupRepository
@Provides get() = this
@Provides
fun ktorfit(http: Http, serverPreferences: ServerPreferences) = Ktorfit
.Builder()
.httpClient(http)
.requestConverter(FlowIORequestConverter())
.baseUrl(serverPreferences.serverUrl().get().toString().addSuffix('/'))
.build()
val CategoryRepositoryImpl.bind: CategoryRepository
@Provides get() = this
@Provides
fun backupRepository(ktorfit: Ktorfit) = ktorfit.createIt<BackupRepository>()
val ChapterRepositoryImpl.bind: ChapterRepository
@Provides get() = this
@Provides
fun categoryRepository(ktorfit: Ktorfit) = ktorfit.createIt<CategoryRepository>()
val DownloadRepositoryImpl.bind: DownloadRepository
@Provides get() = this
@Provides
fun chapterRepository(ktorfit: Ktorfit) = ktorfit.createIt<ChapterRepository>()
val ExtensionRepositoryImpl.bind: ExtensionRepository
@Provides get() = this
@Provides
fun downloadRepository(ktorfit: Ktorfit) = ktorfit.createIt<DownloadRepository>()
val LibraryRepositoryImpl.bind: LibraryRepository
@Provides get() = this
@Provides
fun extensionRepository(ktorfit: Ktorfit) = ktorfit.createIt<ExtensionRepository>()
val MangaRepositoryImpl.bind: MangaRepository
@Provides get() = this
@Provides
fun libraryRepository(ktorfit: Ktorfit) = ktorfit.createIt<LibraryRepository>()
val SettingsRepositoryImpl.bind: SettingsRepository
@Provides get() = this
@Provides
fun mangaRepository(ktorfit: Ktorfit) = ktorfit.createIt<MangaRepository>()
val SourceRepositoryImpl.bind: SourceRepository
@Provides get() = this
@Provides
fun settingsRepository(ktorfit: Ktorfit) = ktorfit.createIt<SettingsRepository>()
val UpdatesRepositoryImpl.bind: UpdatesRepository
@Provides get() = this
@Provides
fun sourceRepository(ktorfit: Ktorfit) = ktorfit.createIt<SourceRepository>()
@Provides
fun updatesRepository(ktorfit: Ktorfit) = ktorfit.createIt<UpdatesRepository>()
}

View File

@@ -0,0 +1,44 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data
import ca.gosyer.jui.core.lang.IO
import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.converter.request.RequestConverter
import de.jensklingenberg.ktorfit.internal.TypeData
import io.ktor.client.call.body
import io.ktor.client.statement.HttpResponse
import io.ktor.util.reflect.TypeInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
class FlowIORequestConverter : RequestConverter {
override fun supportedType(typeData: TypeData, isSuspend: Boolean): Boolean {
return typeData.qualifiedName == "kotlinx.coroutines.flow.Flow"
}
override fun <RequestType> convertRequest(
typeData: TypeData,
requestFunction: suspend () -> Pair<TypeInfo, HttpResponse?>,
ktorfit: Ktorfit
): Any {
return flow {
try {
val (info, response) = requestFunction()
if (info.type == HttpResponse::class) {
emit(response!!)
} else {
emit(response!!.body(info))
}
} catch (exception: Exception) {
throw exception
}
}.flowOn(Dispatchers.IO)
}
}

View File

@@ -1,84 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.backup
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.backup.model.BackupValidationResult
import ca.gosyer.jui.domain.backup.service.BackupRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.backupFileExportRequest
import ca.gosyer.jui.domain.server.model.requests.backupFileImportRequest
import ca.gosyer.jui.domain.server.model.requests.validateBackupFileRequest
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.request.get
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
import okio.FileSystem
import okio.Path
import okio.buffer
class BackupRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), BackupRepository {
private fun buildFormData(file: Path) = formData {
append(
"backup.proto.gz",
FileSystem.SYSTEM.source(file).buffer().readByteArray(),
Headers.build {
append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString())
append(HttpHeaders.ContentDisposition, "filename=backup.proto.gz")
}
)
}
override fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.submitFormWithBinaryData(
buildUrl { path(backupFileImportRequest()) },
formData = buildFormData(file)
) {
expectSuccess = true
block()
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.submitFormWithBinaryData(
buildUrl { path(validateBackupFileRequest()) },
formData = buildFormData(file)
) {
expectSuccess = true
block()
}.body<BackupValidationResult>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun exportBackupFile(block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get(
buildUrl { path(backupFileExportRequest()) }
) {
expectSuccess = true
block()
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,23 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.base
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.http.URLBuilder
open class BaseRepository(
protected val client: Http,
serverPreferences: ServerPreferences
) {
private val _serverUrl = serverPreferences.serverUrl()
val serverUrl get() = _serverUrl.get().toString()
fun buildUrl(builder: URLBuilder.() -> Unit) = URLBuilder(serverUrl).apply(builder).buildString()
fun URLBuilder.parameter(key: String, value: Any) = encodedParameters.append(key, value.toString())
}

View File

@@ -1,144 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.category
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.category.model.Category
import ca.gosyer.jui.domain.category.service.CategoryRepository
import ca.gosyer.jui.domain.manga.model.Manga
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.addMangaToCategoryQuery
import ca.gosyer.jui.domain.server.model.requests.categoryDeleteRequest
import ca.gosyer.jui.domain.server.model.requests.categoryModifyRequest
import ca.gosyer.jui.domain.server.model.requests.categoryReorderRequest
import ca.gosyer.jui.domain.server.model.requests.createCategoryRequest
import ca.gosyer.jui.domain.server.model.requests.getCategoriesQuery
import ca.gosyer.jui.domain.server.model.requests.getMangaCategoriesQuery
import ca.gosyer.jui.domain.server.model.requests.getMangaInCategoryQuery
import ca.gosyer.jui.domain.server.model.requests.removeMangaFromCategoryRequest
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.delete
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.http.HttpMethod
import io.ktor.http.Parameters
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class CategoryRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), CategoryRepository {
override fun getMangaCategories(mangaId: Long) = flow {
val response = client.get(
buildUrl { path(getMangaCategoriesQuery(mangaId)) }
) {
expectSuccess = true
}.body<List<Category>>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun addMangaToCategory(mangaId: Long, categoryId: Long) = flow {
val response = client.get(
buildUrl { path(addMangaToCategoryQuery(mangaId, categoryId)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun removeMangaFromCategory(mangaId: Long, categoryId: Long) = flow {
val response = client.delete(
buildUrl { path(removeMangaFromCategoryRequest(mangaId, categoryId)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun getCategories(dropDefault: Boolean) = flow {
val response = client.get(
buildUrl { path(getCategoriesQuery()) }
) {
expectSuccess = true
}.body<List<Category>>().let { categories ->
if (dropDefault) {
categories.filterNot { it.name.equals("default", true) }
} else categories
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun createCategory(name: String) = flow {
val response = client.submitForm(
buildUrl { path(createCategoryRequest()) },
formParameters = Parameters.build {
append("name", name)
}
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun modifyCategory(categoryId: Long, name: String?, isLanding: Boolean?) = flow {
val response = client.submitForm(
buildUrl { path(categoryModifyRequest(categoryId)) },
formParameters = Parameters.build {
if (name != null) {
append("name", name)
}
if (isLanding != null) {
append("isLanding", isLanding.toString())
}
}
) {
method = HttpMethod.Patch
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun reorderCategory(to: Int, from: Int) = flow {
val response = client.submitForm(
buildUrl { path(categoryReorderRequest()) },
formParameters = Parameters.build {
append("to", to.toString())
append("from", from.toString())
}
) {
method = HttpMethod.Patch
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun deleteCategory(categoryId: Long) = flow {
val response = client.delete(
buildUrl { path(categoryDeleteRequest(categoryId)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun getMangaFromCategory(categoryId: Long) = flow {
val response = client.get(
buildUrl { path(getMangaInCategoryQuery(categoryId)) }
) {
expectSuccess = true
}.body<List<Manga>>()
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,146 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.chapter
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.deleteDownloadedChapterRequest
import ca.gosyer.jui.domain.server.model.requests.getChapterQuery
import ca.gosyer.jui.domain.server.model.requests.getMangaChaptersQuery
import ca.gosyer.jui.domain.server.model.requests.getPageQuery
import ca.gosyer.jui.domain.server.model.requests.queueDownloadChapterRequest
import ca.gosyer.jui.domain.server.model.requests.stopDownloadingChapterRequest
import ca.gosyer.jui.domain.server.model.requests.updateChapterMetaRequest
import ca.gosyer.jui.domain.server.model.requests.updateChapterRequest
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.http.HttpMethod
import io.ktor.http.Parameters
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class ChapterRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), ChapterRepository {
override fun getChapters(mangaId: Long, refresh: Boolean) = flow {
val response = client.get(
buildUrl {
path(getMangaChaptersQuery(mangaId))
if (refresh) {
parameter("onlineFetch", true)
}
}
) {
expectSuccess = true
}.body<List<Chapter>>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getChapter(mangaId: Long, chapterIndex: Int) = flow {
val response = client.get(
buildUrl { path(getChapterQuery(mangaId, chapterIndex)) }
) {
expectSuccess = true
}.body<Chapter>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun updateChapter(
mangaId: Long,
chapterIndex: Int,
read: Boolean?,
bookmarked: Boolean?,
lastPageRead: Int?,
markPreviousRead: Boolean?
) = flow {
val response = client.submitForm(
buildUrl { path(updateChapterRequest(mangaId, chapterIndex)) },
formParameters = Parameters.build {
if (read != null) {
append("read", read.toString())
}
if (bookmarked != null) {
append("bookmarked", bookmarked.toString())
}
if (lastPageRead != null) {
append("lastPageRead", lastPageRead.toString())
}
if (markPreviousRead != null) {
append("markPrevRead", markPreviousRead.toString())
}
}
) {
method = HttpMethod.Patch
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun getPage(mangaId: Long, chapterIndex: Int, pageNum: Int, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get(
buildUrl { path(getPageQuery(mangaId, chapterIndex, pageNum)) }
) {
expectSuccess = true
block()
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun deleteChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.delete(
buildUrl { path(deleteDownloadedChapterRequest(mangaId, chapterIndex)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun queueChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.get(
buildUrl { path(queueDownloadChapterRequest(mangaId, chapterIndex)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun stopChapterDownload(mangaId: Long, chapterIndex: Int) = flow {
val response = client.delete(
buildUrl { path(stopDownloadingChapterRequest(mangaId, chapterIndex)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String) = flow {
val response = client.submitForm(
buildUrl { path(updateChapterMetaRequest(mangaId, chapterIndex)) },
formParameters = Parameters.build {
append("key", key)
append("value", value)
}
) {
method = HttpMethod.Patch
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,56 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.download
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.download.service.DownloadRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.downloadsClearRequest
import ca.gosyer.jui.domain.server.model.requests.downloadsStartRequest
import ca.gosyer.jui.domain.server.model.requests.downloadsStopRequest
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.get
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class DownloadRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), DownloadRepository {
override fun startDownloading() = flow {
val response = client.get(
buildUrl { path(downloadsStartRequest()) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun stopDownloading() = flow {
val response = client.get(
buildUrl { path(downloadsStopRequest()) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun clearDownloadQueue() = flow {
val response = client.get(
buildUrl { path(downloadsClearRequest()) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,81 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.extension
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.extension.model.Extension
import ca.gosyer.jui.domain.extension.service.ExtensionRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.apkIconQuery
import ca.gosyer.jui.domain.server.model.requests.apkInstallQuery
import ca.gosyer.jui.domain.server.model.requests.apkUninstallQuery
import ca.gosyer.jui.domain.server.model.requests.apkUpdateQuery
import ca.gosyer.jui.domain.server.model.requests.extensionListQuery
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class ExtensionRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), ExtensionRepository {
override fun getExtensionList() = flow {
val response = client.get(
buildUrl { path(extensionListQuery()) }
) {
expectSuccess = true
}.body<List<Extension>>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun installExtension(extension: Extension) = flow {
val response = client.get(
buildUrl { path(apkInstallQuery(extension.pkgName)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun updateExtension(extension: Extension) = flow {
val response = client.get(
buildUrl { path(apkUpdateQuery(extension.pkgName)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun uninstallExtension(extension: Extension) = flow {
val response = client.get(
buildUrl { path(apkUninstallQuery(extension.pkgName)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get(
buildUrl { path(apkIconQuery(extension.apkName)) }
) {
expectSuccess = true
block()
}.bodyAsChannel()
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,47 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.library
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.library.service.LibraryRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.addMangaToLibraryQuery
import ca.gosyer.jui.domain.server.model.requests.removeMangaFromLibraryRequest
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class LibraryRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), LibraryRepository {
override fun addMangaToLibrary(mangaId: Long) = flow {
val response = client.get(
buildUrl { path(addMangaToLibraryQuery(mangaId)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun removeMangaFromLibrary(mangaId: Long) = flow {
val response = client.delete(
buildUrl { path(removeMangaFromLibraryRequest(mangaId)) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,74 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.manga
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.manga.model.Manga
import ca.gosyer.jui.domain.manga.service.MangaRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.mangaQuery
import ca.gosyer.jui.domain.server.model.requests.mangaThumbnailQuery
import ca.gosyer.jui.domain.server.model.requests.updateMangaMetaRequest
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.HttpMethod
import io.ktor.http.Parameters
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class MangaRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), MangaRepository {
override fun getManga(mangaId: Long, refresh: Boolean) = flow {
val response = client.get(
buildUrl {
path(mangaQuery(mangaId))
if (refresh) {
parameter("onlineFetch", true)
}
}
) {
expectSuccess = true
}.body<Manga>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit) = flow {
val response = client.get(
buildUrl { path(mangaThumbnailQuery(mangaId)) }
) {
expectSuccess = true
block()
}.bodyAsChannel()
emit(response)
}.flowOn(Dispatchers.IO)
override fun updateMangaMeta(mangaId: Long, key: String, value: String) = flow {
val response = client.submitForm(
buildUrl { path(updateMangaMetaRequest(mangaId)) },
formParameters = Parameters.build {
append("key", key)
append("value", value)
}
) {
method = HttpMethod.Patch
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,49 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.settings
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.aboutQuery
import ca.gosyer.jui.domain.server.model.requests.checkUpdateQuery
import ca.gosyer.jui.domain.server.service.ServerPreferences
import ca.gosyer.jui.domain.settings.model.About
import ca.gosyer.jui.domain.settings.service.SettingsRepository
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class SettingsRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), SettingsRepository {
override fun aboutServer() = flow {
val response = client.get(
buildUrl { path(aboutQuery()) }
) {
expectSuccess = true
}.body<About>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun checkUpdate() = flow {
val response = client.post(
buildUrl { path(checkUpdateQuery()) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -1,162 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.source
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.getFilterListQuery
import ca.gosyer.jui.domain.server.model.requests.getSourceSettingsQuery
import ca.gosyer.jui.domain.server.model.requests.setFilterRequest
import ca.gosyer.jui.domain.server.model.requests.sourceInfoQuery
import ca.gosyer.jui.domain.server.model.requests.sourceLatestQuery
import ca.gosyer.jui.domain.server.model.requests.sourceListQuery
import ca.gosyer.jui.domain.server.model.requests.sourcePopularQuery
import ca.gosyer.jui.domain.server.model.requests.sourceSearchQuery
import ca.gosyer.jui.domain.server.model.requests.updateSourceSettingQuery
import ca.gosyer.jui.domain.server.service.ServerPreferences
import ca.gosyer.jui.domain.source.model.MangaPage
import ca.gosyer.jui.domain.source.model.Source
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilter
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilterChange
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreferenceChange
import ca.gosyer.jui.domain.source.service.SourceRepository
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
class SourceRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), SourceRepository {
override fun getSourceList() = flow {
val response = client.get(
buildUrl { path(sourceListQuery()) }
) {
expectSuccess = true
}.body<List<Source>>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getSourceInfo(sourceId: Long) = flow {
val response = client.get(
buildUrl { path(sourceInfoQuery(sourceId)) }
) {
expectSuccess = true
}.body<Source>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getPopularManga(sourceId: Long, pageNum: Int) = flow {
val response = client.get(
buildUrl { path(sourcePopularQuery(sourceId, pageNum)) }
) {
expectSuccess = true
}.body<MangaPage>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getLatestManga(sourceId: Long, pageNum: Int) = flow {
val response = client.get(
buildUrl { path(sourceLatestQuery(sourceId, pageNum)) }
) {
expectSuccess = true
}.body<MangaPage>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int) = flow {
val response = client.get(
buildUrl {
path(sourceSearchQuery(sourceId))
parameter("pageNum", pageNum)
if (searchTerm.isNotBlank()) {
parameter("searchTerm", searchTerm)
}
}
) {
expectSuccess = true
}.body<MangaPage>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun getFilterList(sourceId: Long, reset: Boolean) = flow {
val response = client.get(
buildUrl {
path(getFilterListQuery(sourceId))
if (reset) {
parameter("reset", true)
}
}
) {
expectSuccess = true
}.body<List<SourceFilter>>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange) = flow {
val response = client.post(
buildUrl { path(setFilterRequest(sourceId)) }
) {
contentType(ContentType.Application.Json)
setBody(sourceFilter)
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun setFilter(sourceId: Long, position: Int, value: Any) = setFilter(
sourceId,
SourceFilterChange(position, value)
)
override fun setFilter(sourceId: Long, parentPosition: Int, childPosition: Int, value: Any) = setFilter(
sourceId,
SourceFilterChange(
parentPosition,
Json.encodeToString(SourceFilterChange(childPosition, value))
)
)
override fun getSourceSettings(sourceId: Long) = flow {
val response = client.get(
buildUrl { path(getSourceSettingsQuery(sourceId)) }
) {
expectSuccess = true
}.body<List<SourcePreference>>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange) = flow {
val response = client.post(
buildUrl { path(updateSourceSettingQuery(sourceId)) }
) {
contentType(ContentType.Application.Json)
setBody(sourcePreference)
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun setSourceSetting(sourceId: Long, position: Int, value: Any) = setSourceSetting(
sourceId,
SourcePreferenceChange(position, value)
)
}

View File

@@ -1,63 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.data.updates
import ca.gosyer.jui.core.lang.IO
import ca.gosyer.jui.data.base.BaseRepository
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.fetchUpdatesRequest
import ca.gosyer.jui.domain.server.model.requests.recentUpdatesQuery
import ca.gosyer.jui.domain.server.service.ServerPreferences
import ca.gosyer.jui.domain.updates.model.Updates
import ca.gosyer.jui.domain.updates.service.UpdatesRepository
import io.ktor.client.call.body
import io.ktor.client.plugins.expectSuccess
import io.ktor.client.request.forms.submitForm
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.http.Parameters
import io.ktor.http.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class UpdatesRepositoryImpl @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
) : BaseRepository(client, serverPreferences), UpdatesRepository {
override fun getRecentUpdates(pageNum: Int) = flow {
val response = client.get(
buildUrl { path(recentUpdatesQuery(pageNum)) }
) {
expectSuccess = true
}.body<Updates>()
emit(response)
}.flowOn(Dispatchers.IO)
override fun updateLibrary() = flow {
val response = client.post(
buildUrl { path(fetchUpdatesRequest()) }
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
override fun updateCategory(categoryId: Long) = flow {
val response = client.submitForm(
buildUrl { path(fetchUpdatesRequest()) },
formParameters = Parameters.build {
append("category", categoryId.toString())
}
) {
expectSuccess = true
}
emit(response)
}.flowOn(Dispatchers.IO)
}

View File

@@ -63,6 +63,10 @@ dependencies {
implementation(libs.ktor.websockets)
implementation(libs.ktor.auth)
// Ktorfit
implementation(libs.ktorfit.lib)
ksp(libs.ktorfit.ksp)
// Logging
implementation(libs.logging.slf4j.api)
implementation(libs.logging.slf4j.jul)

View File

@@ -32,6 +32,7 @@ kotlin {
languageSettings {
optIn("kotlin.RequiresOptIn")
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
optIn("de.jensklingenberg.ktorfit.internal.InternalKtorfitApi")
}
}
val commonMain by getting {
@@ -48,6 +49,7 @@ kotlin {
api(libs.ktor.auth)
api(libs.ktor.logging)
api(libs.ktor.websockets)
api(libs.ktorfit.lib)
api(libs.okio)
api(libs.dateTime)
api(projects.core)
@@ -113,6 +115,9 @@ kotlin {
dependencies {
add("kspDesktop", libs.kotlinInject.compiler)
add("kspAndroid", libs.kotlinInject.compiler)
add("kspCommonMainMetadata", libs.ktorfit.ksp)
add("kspDesktop", libs.ktorfit.ksp)
add("kspAndroid", libs.ktorfit.ksp)
}
buildkonfig {

View File

@@ -4,14 +4,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
package ca.gosyer.jui.domain
annotation class Get
import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.create
annotation class Post
annotation class Delete
annotation class Patch
annotation class WS
inline fun <reified T> Ktorfit.createIt(): T = create()

View File

@@ -21,7 +21,7 @@ class ImportBackupFile @Inject constructor(private val backupRepository: BackupR
.singleOrNull()
fun asFlow(file: Path, block: HttpRequestBuilder.() -> Unit = {}) =
backupRepository.importBackupFile(file, block)
backupRepository.importBackupFile(BackupRepository.buildBackupFormData(file), block)
companion object {
private val log = logging()

View File

@@ -21,7 +21,7 @@ class ValidateBackupFile @Inject constructor(private val backupRepository: Backu
.singleOrNull()
fun asFlow(file: Path, block: HttpRequestBuilder.() -> Unit = {}) =
backupRepository.validateBackupFile(file, block)
backupRepository.validateBackupFile(BackupRepository.buildBackupFormData(file), block)
companion object {
private val log = logging()

View File

@@ -6,14 +6,55 @@
package ca.gosyer.jui.domain.backup.service
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.domain.backup.model.BackupValidationResult
import de.jensklingenberg.ktorfit.http.Multipart
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Part
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.forms.formData
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.content.PartData
import kotlinx.coroutines.flow.Flow
import okio.FileSystem
import okio.Path
import okio.buffer
interface BackupRepository {
fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}): Flow<HttpResponse>
fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}): Flow<BackupValidationResult>
fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}): Flow<HttpResponse>
@Multipart
@POST("api/v1/backup/import/file")
fun importBackupFile(
@Part("") formData: List<PartData>,
@ReqBuilder block: HttpRequestBuilder.() -> Unit = {}
): Flow<HttpResponse>
@Multipart
@POST("api/v1/backup/validate/file")
fun validateBackupFile(
@Part("") formData: List<PartData>,
@ReqBuilder block: HttpRequestBuilder.() -> Unit = {}
): Flow<BackupValidationResult>
@POST("api/v1/backup/export/file")
fun exportBackupFile(
@ReqBuilder block: HttpRequestBuilder.() -> Unit = {}
): Flow<HttpResponse>
companion object {
fun buildBackupFormData(file: Path) = formData {
append(
"backup.proto.gz",
FileSystem.SYSTEM.source(file).buffer().readByteArray(),
Headers.build {
append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString())
append(HttpHeaders.ContentDisposition, "filename=backup.proto.gz")
}
)
}
}
}

View File

@@ -8,6 +8,7 @@ package ca.gosyer.jui.domain.category.interactor
import ca.gosyer.jui.domain.category.service.CategoryRepository
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
@@ -18,7 +19,12 @@ class GetCategories @Inject constructor(private val categoryRepository: Category
.catch { log.warn(it) { "Failed to get categories" } }
.singleOrNull()
fun asFlow(dropDefault: Boolean = false) = categoryRepository.getCategories(dropDefault)
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

@@ -15,28 +15,24 @@ import org.lighthousegames.logging.logging
class ModifyCategory @Inject constructor(private val categoryRepository: CategoryRepository) {
suspend fun await(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = asFlow(
suspend fun await(categoryId: Long, name: String) = asFlow(
categoryId = categoryId,
name = name,
isLanding = isLanding
).catch { log.warn(it) { "Failed to modify category $categoryId with options: name=$name,isLanding=$isLanding" } }.collect()
name = name
).catch { log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } }.collect()
suspend fun await(category: Category, name: String? = null, isLanding: Boolean? = null) = asFlow(
suspend fun await(category: Category, name: String? = null) = asFlow(
category = category,
name = name,
isLanding = isLanding
).catch { log.warn(it) { "Failed to modify category ${category.name} with options: name=$name,isLanding=$isLanding" } }.collect()
name = name
).catch { log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } }.collect()
fun asFlow(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = categoryRepository.modifyCategory(
fun asFlow(categoryId: Long, name: String) = categoryRepository.modifyCategory(
categoryId = categoryId,
name = name,
isLanding = isLanding
name = name
)
fun asFlow(category: Category, name: String? = null, isLanding: Boolean? = null) = categoryRepository.modifyCategory(
fun asFlow(category: Category, name: String? = null) = categoryRepository.modifyCategory(
categoryId = category.id,
name = name,
isLanding = isLanding
name = name ?: category.name
)
companion object {

View File

@@ -8,17 +8,64 @@ package ca.gosyer.jui.domain.category.service
import ca.gosyer.jui.domain.category.model.Category
import ca.gosyer.jui.domain.manga.model.Manga
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface CategoryRepository {
fun getMangaCategories(mangaId: Long): Flow<List<Category>>
fun addMangaToCategory(mangaId: Long, categoryId: Long): Flow<HttpResponse>
fun removeMangaFromCategory(mangaId: Long, categoryId: Long): Flow<HttpResponse>
fun getCategories(dropDefault: Boolean = false): Flow<List<Category>>
fun createCategory(name: String): Flow<HttpResponse>
fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null): Flow<HttpResponse>
fun reorderCategory(to: Int, from: Int): Flow<HttpResponse>
fun deleteCategory(categoryId: Long): Flow<HttpResponse>
fun getMangaFromCategory(categoryId: Long): Flow<List<Manga>>
@GET("api/v1/manga/{mangaId}/category/")
fun getMangaCategories(
@Path("mangaId") mangaId: Long
): Flow<List<Category>>
@GET("api/v1/manga/{mangaId}/category/{categoryId}")
fun addMangaToCategory(
@Path("mangaId") mangaId: Long,
@Path("categoryId") categoryId: Long
): Flow<HttpResponse>
@DELETE("api/v1/manga/{mangaId}/category/{categoryId}")
fun removeMangaFromCategory(
@Path("mangaId") mangaId: Long,
@Path("categoryId") categoryId: Long
): Flow<HttpResponse>
@GET("api/v1/category/")
fun getCategories(): Flow<List<Category>>
@FormUrlEncoded
@POST("api/v1/category/")
fun createCategory(
@Field("name") name: String
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/category/{categoryId}")
fun modifyCategory(
@Path("categoryId") categoryId: Long,
@Field("name") name: String
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/category/reorder")
fun reorderCategory(
@Field("to") to: Int,
@Field("from") from: Int
): Flow<HttpResponse>
@DELETE("api/v1/category/{categoryId}")
fun deleteCategory(
@Path("categoryId") categoryId: Long
): Flow<HttpResponse>
@GET("api/v1/category/{categoryId}")
fun getMangaFromCategory(
@Path("categoryId") categoryId: Long
): Flow<List<Manga>>
}

View File

@@ -0,0 +1,74 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.chapter.interactor
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.manga.model.Manga
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class UpdateChapterBookmarked @Inject constructor(private val chapterRepository: ChapterRepository) {
suspend fun await(
mangaId: Long,
index: Int,
bookmarked: Boolean,
) = asFlow(mangaId, index, bookmarked)
.catch { log.warn(it) { "Failed to update chapter bookmark for chapter $index of $mangaId" } }
.collect()
suspend fun await(
manga: Manga,
index: Int,
bookmarked: Boolean,
) = asFlow(manga, index, bookmarked)
.catch { log.warn(it) { "Failed to update chapter bookmark for chapter $index of ${manga.title}(${manga.id})" } }
.collect()
suspend fun await(
chapter: Chapter,
bookmarked: Boolean,
) = asFlow(chapter, bookmarked)
.catch { log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" } }
.collect()
fun asFlow(
mangaId: Long,
index: Int,
bookmarked: Boolean,
) = chapterRepository.updateChapterBookmarked(
mangaId = mangaId,
chapterIndex = index,
bookmarked = bookmarked,
)
fun asFlow(
manga: Manga,
index: Int,
bookmarked: Boolean,
) = chapterRepository.updateChapterBookmarked(
mangaId = manga.id,
chapterIndex = index,
bookmarked = bookmarked,
)
fun asFlow(
chapter: Chapter,
bookmarked: Boolean,
) = chapterRepository.updateChapterBookmarked(
mangaId = chapter.mangaId,
chapterIndex = chapter.index,
bookmarked = bookmarked,
)
companion object {
private val log = logging()
}
}

View File

@@ -1,101 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.chapter.interactor
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.manga.model.Manga
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class UpdateChapterFlags @Inject constructor(private val chapterRepository: ChapterRepository) {
suspend fun await(
mangaId: Long,
index: Int,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = asFlow(mangaId, index, read, bookmarked, lastPageRead, markPreviousRead)
.catch { log.warn(it) { "Failed to update chapter flags for chapter $index of $mangaId" } }
.collect()
suspend fun await(
manga: Manga,
index: Int,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = asFlow(manga, index, read, bookmarked, lastPageRead, markPreviousRead)
.catch { log.warn(it) { "Failed to update chapter flags for chapter $index of ${manga.title}(${manga.id})" } }
.collect()
suspend fun await(
chapter: Chapter,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = asFlow(chapter, read, bookmarked, lastPageRead, markPreviousRead)
.catch { log.warn(it) { "Failed to update chapter flags for chapter ${chapter.index} of ${chapter.mangaId}" } }
.collect()
fun asFlow(
mangaId: Long,
index: Int,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = chapterRepository.updateChapter(
mangaId = mangaId,
chapterIndex = index,
read = read,
bookmarked = bookmarked,
lastPageRead = lastPageRead,
markPreviousRead = markPreviousRead
)
fun asFlow(
manga: Manga,
index: Int,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = chapterRepository.updateChapter(
mangaId = manga.id,
chapterIndex = index,
read = read,
bookmarked = bookmarked,
lastPageRead = lastPageRead,
markPreviousRead = markPreviousRead
)
fun asFlow(
chapter: Chapter,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
) = chapterRepository.updateChapter(
mangaId = chapter.mangaId,
chapterIndex = chapter.index,
read = read,
bookmarked = bookmarked,
lastPageRead = lastPageRead,
markPreviousRead = markPreviousRead
)
companion object {
private val log = logging()
}
}

View File

@@ -0,0 +1,74 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.chapter.interactor
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.manga.model.Manga
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class UpdateChapterLastPageRead @Inject constructor(private val chapterRepository: ChapterRepository) {
suspend fun await(
mangaId: Long,
index: Int,
lastPageRead: Int,
) = asFlow(mangaId, index, lastPageRead)
.catch { log.warn(it) { "Failed to update chapter last page read for chapter $index of $mangaId" } }
.collect()
suspend fun await(
manga: Manga,
index: Int,
lastPageRead: Int,
) = asFlow(manga, index, lastPageRead)
.catch { log.warn(it) { "Failed to update chapter last page read for chapter $index of ${manga.title}(${manga.id})" } }
.collect()
suspend fun await(
chapter: Chapter,
lastPageRead: Int,
) = asFlow(chapter, lastPageRead)
.catch { log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" } }
.collect()
fun asFlow(
mangaId: Long,
index: Int,
lastPageRead: Int,
) = chapterRepository.updateChapterLastPageRead(
mangaId = mangaId,
chapterIndex = index,
lastPageRead = lastPageRead,
)
fun asFlow(
manga: Manga,
index: Int,
lastPageRead: Int,
) = chapterRepository.updateChapterLastPageRead(
mangaId = manga.id,
chapterIndex = index,
lastPageRead = lastPageRead,
)
fun asFlow(
chapter: Chapter,
lastPageRead: Int,
) = chapterRepository.updateChapterLastPageRead(
mangaId = chapter.mangaId,
chapterIndex = chapter.index,
lastPageRead = lastPageRead,
)
companion object {
private val log = logging()
}
}

View File

@@ -0,0 +1,65 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.chapter.interactor
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.manga.model.Manga
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class UpdateChapterMarkPreviousRead @Inject constructor(private val chapterRepository: ChapterRepository) {
suspend fun await(
mangaId: Long,
index: Int
) = asFlow(mangaId, index)
.catch { log.warn(it) { "Failed to update chapter read status for chapter $index of $mangaId" } }
.collect()
suspend fun await(
manga: Manga,
index: Int
) = asFlow(manga, index)
.catch { log.warn(it) { "Failed to update chapter read status for chapter $index of ${manga.title}(${manga.id})" } }
.collect()
suspend fun await(
chapter: Chapter
) = asFlow(chapter)
.catch { log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } }
.collect()
fun asFlow(
mangaId: Long,
index: Int,
) = chapterRepository.updateChapterMarkPrevRead(
mangaId = mangaId,
chapterIndex = index
)
fun asFlow(
manga: Manga,
index: Int,
) = chapterRepository.updateChapterMarkPrevRead(
mangaId = manga.id,
chapterIndex = index,
)
fun asFlow(
chapter: Chapter,
) = chapterRepository.updateChapterMarkPrevRead(
mangaId = chapter.mangaId,
chapterIndex = chapter.index,
)
companion object {
private val log = logging()
}
}

View File

@@ -0,0 +1,74 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.chapter.interactor
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.chapter.service.ChapterRepository
import ca.gosyer.jui.domain.manga.model.Manga
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class UpdateChapterRead @Inject constructor(private val chapterRepository: ChapterRepository) {
suspend fun await(
mangaId: Long,
index: Int,
read: Boolean,
) = asFlow(mangaId, index, read)
.catch { log.warn(it) { "Failed to update chapter read status for chapter $index of $mangaId" } }
.collect()
suspend fun await(
manga: Manga,
index: Int,
read: Boolean,
) = asFlow(manga, index, read)
.catch { log.warn(it) { "Failed to update chapter read status for chapter $index of ${manga.title}(${manga.id})" } }
.collect()
suspend fun await(
chapter: Chapter,
read: Boolean,
) = asFlow(chapter, read)
.catch { log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } }
.collect()
fun asFlow(
mangaId: Long,
index: Int,
read: Boolean,
) = chapterRepository.updateChapterRead(
mangaId = mangaId,
chapterIndex = index,
read = read,
)
fun asFlow(
manga: Manga,
index: Int,
read: Boolean,
) = chapterRepository.updateChapterRead(
mangaId = manga.id,
chapterIndex = index,
read = read,
)
fun asFlow(
chapter: Chapter,
read: Boolean,
) = chapterRepository.updateChapterRead(
mangaId = chapter.mangaId,
chapterIndex = chapter.index,
read = read,
)
companion object {
private val log = logging()
}
}

View File

@@ -7,31 +7,109 @@
package ca.gosyer.jui.domain.chapter.service
import ca.gosyer.jui.domain.chapter.model.Chapter
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface ChapterRepository {
fun getChapters(mangaId: Long, refresh: Boolean = false): Flow<List<Chapter>>
fun getChapter(mangaId: Long, chapterIndex: Int): Flow<Chapter>
@GET("api/v1/manga/{mangaId}/chapters")
fun getChapters(
@Path("mangaId") mangaId: Long,
@Query("onlineFetch") refresh: Boolean = false
): Flow<List<Chapter>>
@GET("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun getChapter(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int
): Flow<Chapter>
/* TODO add once ktorfit supports nullable paremters
@FormUrlEncoded
@PATCH("/api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun updateChapter(
mangaId: Long,
chapterIndex: Int,
read: Boolean? = null,
bookmarked: Boolean? = null,
lastPageRead: Int? = null,
markPreviousRead: Boolean? = null
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("read") read: Boolean? = null,
@Field("bookmarked") bookmarked: Boolean? = null,
@Field("lastPageRead") lastPageRead: Int? = null,
@Field("markPrevRead") markPreviousRead: Boolean? = null
): Flow<HttpResponse>*/
//todo remove following updateChapter functions once ktorfit supports nullable parameters
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun updateChapterRead(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("read") read: Boolean,
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun updateChapterBookmarked(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("bookmarked") bookmarked: Boolean,
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun updateChapterLastPageRead(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("lastPageRead") lastPageRead: Int,
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun updateChapterMarkPrevRead(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("markPrevRead") markPreviousRead: Boolean = true
): Flow<HttpResponse>
@GET("api/v1/manga/{mangaId}/chapter/{chapterIndex}/page/{pageNum}")
fun getPage(
mangaId: Long,
chapterIndex: Int,
pageNum: Int,
block: HttpRequestBuilder.() -> Unit
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Path("pageNum") pageNum: Int,
@ReqBuilder block: HttpRequestBuilder.() -> Unit
): Flow<HttpResponse>
fun deleteChapterDownload(mangaId: Long, chapterIndex: Int): Flow<HttpResponse>
fun queueChapterDownload(mangaId: Long, chapterIndex: Int): Flow<HttpResponse>
fun stopChapterDownload(mangaId: Long, chapterIndex: Int): Flow<HttpResponse>
fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String): Flow<HttpResponse>
@DELETE("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun deleteChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int
): Flow<HttpResponse>
@GET("api/v1/download/{mangaId}/chapter/{chapterIndex}")
fun queueChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int
): Flow<HttpResponse>
@DELETE("api/v1/download/{mangaId}/chapter/{chapterIndex}")
fun stopChapterDownload(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int
): Flow<HttpResponse>
@FormUrlEncoded
@PATCH("api/v1/manga/{mangaId}/chapter/{chapterIndex}/meta")
fun updateChapterMeta(
@Path("mangaId") mangaId: Long,
@Path("chapterIndex") chapterIndex: Int,
@Field("key") key: String,
@Field("value") value: String
): Flow<HttpResponse>
}

View File

@@ -6,11 +6,17 @@
package ca.gosyer.jui.domain.download.service
import de.jensklingenberg.ktorfit.http.GET
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface DownloadRepository {
@GET("api/v1/downloads/start")
fun startDownloading(): Flow<HttpResponse>
@GET("api/v1/downloads/stop")
fun stopDownloading(): Flow<HttpResponse>
@GET("api/v1/downloads/clear")
fun clearDownloadQueue(): Flow<HttpResponse>
}

View File

@@ -11,7 +11,6 @@ import ca.gosyer.jui.domain.download.model.DownloadChapter
import ca.gosyer.jui.domain.download.model.DownloadStatus
import ca.gosyer.jui.domain.download.model.DownloaderStatus
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.downloadsQuery
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
@@ -28,7 +27,7 @@ class DownloadService @Inject constructor(
get() = status
override val query: String
get() = downloadsQuery()
get() = "/api/v1/downloads"
override suspend fun onReceived(frame: Frame.Text) {
val status = json.decodeFromString<DownloadStatus>(frame.readText())

View File

@@ -19,7 +19,7 @@ class InstallExtension @Inject constructor(private val extensionRepository: Exte
.catch { log.warn(it) { "Failed to install extension ${extension.apkName}" } }
.collect()
fun asFlow(extension: Extension) = extensionRepository.installExtension(extension)
fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName)
companion object {
private val log = logging()

View File

@@ -19,7 +19,7 @@ class UninstallExtension @Inject constructor(private val extensionRepository: Ex
.catch { log.warn(it) { "Failed to uninstall extension ${extension.apkName}" } }
.collect()
fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension)
fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName)
companion object {
private val log = logging()

View File

@@ -19,7 +19,7 @@ class UpdateExtension @Inject constructor(private val extensionRepository: Exten
.catch { log.warn(it) { "Failed to update extension ${extension.apkName}" } }
.collect()
fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension)
fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName)
companion object {
private val log = logging()

View File

@@ -7,15 +7,36 @@
package ca.gosyer.jui.domain.extension.service
import ca.gosyer.jui.domain.extension.model.Extension
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.flow.Flow
interface ExtensionRepository {
@GET("api/v1/extension/list")
fun getExtensionList(): Flow<List<Extension>>
fun installExtension(extension: Extension): Flow<HttpResponse>
fun updateExtension(extension: Extension): Flow<HttpResponse>
fun uninstallExtension(extension: Extension): Flow<HttpResponse>
fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit): Flow<ByteReadChannel>
@GET("api/v1/extension/install/{pkgName}")
fun installExtension(
@Path("pkgName") pkgName: String
): Flow<HttpResponse>
@GET("api/v1/extension/update/{pkgName}")
fun updateExtension(
@Path("pkgName") pkgName: String
): Flow<HttpResponse>
@GET("api/v1/extension/uninstall/{pkgName}")
fun uninstallExtension(
@Path("pkgName") pkgName: String
): Flow<HttpResponse>
@GET("api/v1/extension/icon/{apkName}")
fun getApkIcon(
@Path("apkName") apkName: String,
@ReqBuilder block: HttpRequestBuilder.() -> Unit
): Flow<ByteReadChannel>
}

View File

@@ -6,10 +6,21 @@
package ca.gosyer.jui.domain.library.service
import de.jensklingenberg.ktorfit.http.DELETE
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface LibraryRepository {
fun addMangaToLibrary(mangaId: Long): Flow<HttpResponse>
fun removeMangaFromLibrary(mangaId: Long): Flow<HttpResponse>
@GET("api/v1/manga/{mangaId}/library")
fun addMangaToLibrary(
@Path("mangaId") mangaId: Long
): Flow<HttpResponse>
@DELETE("api/v1/manga/{mangaId}/library")
fun removeMangaFromLibrary(
@Path("mangaId") mangaId: Long
): Flow<HttpResponse>
}

View File

@@ -9,7 +9,6 @@ package ca.gosyer.jui.domain.library.service
import ca.gosyer.jui.domain.base.WebsocketService
import ca.gosyer.jui.domain.library.model.UpdateStatus
import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.model.requests.updatesQuery
import ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
@@ -26,7 +25,7 @@ class LibraryUpdateService @Inject constructor(
override val _status: MutableStateFlow<Status> = MutableStateFlow(Status.STARTING)
override val query: String
get() = updatesQuery()
get() = "/api/v1/update"
override suspend fun onReceived(frame: Frame.Text) {
val status = json.decodeFromString<UpdateStatus>(frame.readText())

View File

@@ -7,13 +7,36 @@
package ca.gosyer.jui.domain.manga.service
import ca.gosyer.jui.domain.manga.model.Manga
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.PATCH
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import de.jensklingenberg.ktorfit.http.ReqBuilder
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.flow.Flow
interface MangaRepository {
fun getManga(mangaId: Long, refresh: Boolean = false): Flow<Manga>
fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit): Flow<ByteReadChannel>
fun updateMangaMeta(mangaId: Long, key: String, value: String): Flow<HttpResponse>
@GET("api/v1/manga/{mangaId}/")
fun getManga(
@Path("mangaId") mangaId: Long,
@Query("onlineFetch") refresh: Boolean = false
): Flow<Manga>
@GET("api/v1/manga/{mangaId}/thumbnail")
fun getMangaThumbnail(
@Path("mangaId") mangaId: Long,
@ReqBuilder block: HttpRequestBuilder.() -> Unit
): Flow<ByteReadChannel>
@PATCH("api/v1/manga/{mangaId}/meta")
@FormUrlEncoded
fun updateMangaMeta(
@Path("mangaId") mangaId: Long,
@Field("key") key: String,
@Field("value") value: String
): Flow<HttpResponse>
}

View File

@@ -1,27 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Post
fun backupImportRequest() =
"/api/v1/backup/import"
@Post
fun backupFileImportRequest() =
"/api/v1/backup/import/file"
@Post
fun backupExportRequest() =
"/api/v1/backup/export"
@Post
fun backupFileExportRequest() =
"/api/v1/backup/export/file"
@Post
fun validateBackupFileRequest() =
"/api/v1/backup/validate/file"

View File

@@ -1,46 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun getMangaCategoriesQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/category/"
@Get
fun addMangaToCategoryQuery(mangaId: Long, categoryId: Long) =
"/api/v1/manga/$mangaId/category/$categoryId"
@Delete
fun removeMangaFromCategoryRequest(mangaId: Long, categoryId: Long) =
"/api/v1/manga/$mangaId/category/$categoryId"
@Get
fun getCategoriesQuery() =
"/api/v1/category/"
/**
* Post a formbody with the param {name} for creation of a category
*/
@Post
fun createCategoryRequest() =
"/api/v1/category/"
@Patch
fun categoryModifyRequest(categoryId: Long) =
"/api/v1/category/$categoryId"
@Patch
fun categoryReorderRequest() =
"/api/v1/category/reorder"
@Delete
fun categoryDeleteRequest(categoryId: Long) =
"/api/v1/category/$categoryId"
@Get
fun getMangaInCategoryQuery(categoryId: Long) =
"/api/v1/category/$categoryId"

View File

@@ -1,39 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun getMangaChaptersQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/chapters"
@Get
fun getChapterQuery(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
@Patch
fun updateChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
@Get
fun getPageQuery(mangaId: Long, chapterIndex: Int, index: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex/page/$index"
@Delete
fun deleteDownloadedChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex"
@Get
fun queueDownloadChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/download/$mangaId/chapter/$chapterIndex"
@Delete
fun stopDownloadingChapterRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/download/$mangaId/chapter/$chapterIndex"
@Patch
fun updateChapterMetaRequest(mangaId: Long, chapterIndex: Int) =
"/api/v1/manga/$mangaId/chapter/$chapterIndex/meta"

View File

@@ -1,23 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@WS
fun downloadsQuery() =
"/api/v1/downloads"
@Get
fun downloadsStartRequest() =
"/api/v1/downloads/start"
@Get
fun downloadsStopRequest() =
"/api/v1/downloads/stop"
@Get
fun downloadsClearRequest() =
"/api/v1/downloads/clear"

View File

@@ -1,27 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun extensionListQuery() =
"/api/v1/extension/list"
@Get
fun apkInstallQuery(pkgName: String) =
"/api/v1/extension/install/$pkgName"
@Get
fun apkUpdateQuery(pkgName: String) =
"/api/v1/extension/update/$pkgName"
@Get
fun apkUninstallQuery(pkgName: String) =
"/api/v1/extension/uninstall/$pkgName"
@Get
fun apkIconQuery(apkName: String) =
"/api/v1/extension/icon/$apkName"

View File

@@ -1,15 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun addMangaToLibraryQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/library"
@Delete
fun removeMangaFromLibraryRequest(mangaId: Long) =
"/api/v1/manga/$mangaId/library"

View File

@@ -1,19 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun mangaQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/"
@Get
fun mangaThumbnailQuery(mangaId: Long) =
"/api/v1/manga/$mangaId/thumbnail"
@Post
fun updateMangaMetaRequest(mangaId: Long) =
"/api/v1/manga/$mangaId/meta"

View File

@@ -1,15 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun aboutQuery() =
"/api/v1/settings/about"
@Get
fun checkUpdateQuery() =
"/api/v1/settings/check-update"

View File

@@ -1,47 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun sourceListQuery() =
"/api/v1/source/list"
@Get
fun sourceInfoQuery(sourceId: Long) =
"/api/v1/source/$sourceId"
@Get
fun sourcePopularQuery(sourceId: Long, pageNum: Int) =
"/api/v1/source/$sourceId/popular/$pageNum"
@Get
fun sourceLatestQuery(sourceId: Long, pageNum: Int) =
"/api/v1/source/$sourceId/latest/$pageNum"
@Get
fun globalSearchQuery() =
"/api/v1/source/all/search"
@Get
fun sourceSearchQuery(sourceId: Long) =
"/api/v1/source/$sourceId/search"
@Get
fun getFilterListQuery(sourceId: Long) =
"/api/v1/source/$sourceId/filters"
@Post
fun setFilterRequest(sourceId: Long) =
"/api/v1/source/$sourceId/filters"
@Get
fun getSourceSettingsQuery(sourceId: Long) =
"/api/v1/source/$sourceId/preferences"
@Post
fun updateSourceSettingQuery(sourceId: Long) =
"/api/v1/source/$sourceId/preferences"

View File

@@ -1,23 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.jui.domain.server.model.requests
@Get
fun recentUpdatesQuery(pageNum: Int) =
"/api/v1/update/recentChapters/$pageNum"
@Post
fun fetchUpdatesRequest() =
"/api/v1/update/fetch"
@Get
fun updatesSummaryQuery() =
"/api/v1/update/summary"
@WS
fun updatesQuery() =
"/api/v1/update"

View File

@@ -7,10 +7,15 @@
package ca.gosyer.jui.domain.settings.service
import ca.gosyer.jui.domain.settings.model.About
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface SettingsRepository {
@GET("api/v1/settings/about")
fun aboutServer(): Flow<About>
@POST("api/v1/settings/check-update")
fun checkUpdate(): Flow<HttpResponse>
}

View File

@@ -12,20 +12,62 @@ import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilter
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilterChange
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreferenceChange
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import de.jensklingenberg.ktorfit.http.Query
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface SourceRepository {
@GET("api/v1/source/list")
fun getSourceList(): Flow<List<Source>>
fun getSourceInfo(sourceId: Long): Flow<Source>
fun getPopularManga(sourceId: Long, pageNum: Int): Flow<MangaPage>
fun getLatestManga(sourceId: Long, pageNum: Int): Flow<MangaPage>
fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int): Flow<MangaPage>
fun getFilterList(sourceId: Long, reset: Boolean = false): Flow<List<SourceFilter>>
fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange): Flow<HttpResponse>
fun setFilter(sourceId: Long, position: Int, value: Any): Flow<HttpResponse>
fun setFilter(sourceId: Long, parentPosition: Int, childPosition: Int, value: Any): Flow<HttpResponse>
fun getSourceSettings(sourceId: Long): Flow<List<SourcePreference>>
fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange): Flow<HttpResponse>
fun setSourceSetting(sourceId: Long, position: Int, value: Any): Flow<HttpResponse>
@GET("api/v1/source/{sourceId}")
fun getSourceInfo(
@Path("sourceId") sourceId: Long
): Flow<Source>
@GET("api/v1/source/{sourceId}/popular/{pageNum}")
fun getPopularManga(
@Path("sourceId") sourceId: Long,
@Path("pageNum") pageNum: Int
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/latest/{pageNum}")
fun getLatestManga(
@Path("sourceId") sourceId: Long,
@Path("pageNum") pageNum: Int
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/search")
fun getSearchResults(
@Path("sourceId") sourceId: Long,
@Query("searchTerm") searchTerm: String?,
@Query("pageNum") pageNum: Int
): Flow<MangaPage>
@GET("api/v1/source/{sourceId}/filters")
fun getFilterList(
@Path("sourceId") sourceId: Long,
@Query("reset") reset: Boolean = false
): Flow<List<SourceFilter>>
@POST("api/v1/source/{sourceId}/filters")
fun setFilter(
@Path("sourceId") sourceId: Long,
@Body sourceFilter: SourceFilterChange
): Flow<HttpResponse>
@GET("api/v1/source/{sourceId}/preferences")
fun getSourceSettings(
@Path("sourceId") sourceId: Long
): Flow<List<SourcePreference>>
@POST("api/v1/source/{sourceId}/preferences")
fun setSourceSetting(
@Path("sourceId") sourceId: Long,
@Body sourcePreference: SourcePreferenceChange
): Flow<HttpResponse>
}

View File

@@ -7,11 +7,26 @@
package ca.gosyer.jui.domain.updates.service
import ca.gosyer.jui.domain.updates.model.Updates
import de.jensklingenberg.ktorfit.http.Field
import de.jensklingenberg.ktorfit.http.FormUrlEncoded
import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.Path
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow
interface UpdatesRepository {
fun getRecentUpdates(pageNum: Int): Flow<Updates>
@GET("api/v1/update/recentChapters/{pageNum}/")
fun getRecentUpdates(
@Path("pageNum") pageNum: Int
): Flow<Updates>
@POST("api/v1/update/fetch/")
fun updateLibrary(): Flow<HttpResponse>
fun updateCategory(categoryId: Long): Flow<HttpResponse>
@POST("api/v1/update/fetch/")
@FormUrlEncoded
fun updateCategory(
@Field("category") categoryId: Long
): Flow<HttpResponse>
}

View File

@@ -37,6 +37,7 @@ kotlinInject = "0.5.1"
# Network
ktor = "2.1.1"
ktorfit = "1.0.0-beta14"
# Logging
slf4j = "1.7.36"
@@ -133,6 +134,8 @@ ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json",
ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
ktor-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }
ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" }
ktorfit-ksp = { module = "de.jensklingenberg.ktorfit:ktorfit-ksp", version.ref = "ktorfit" }
# Logging
logging-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }

View File

@@ -18,7 +18,9 @@ import ca.gosyer.jui.domain.chapter.interactor.GetChapters
import ca.gosyer.jui.domain.chapter.interactor.QueueChapterDownload
import ca.gosyer.jui.domain.chapter.interactor.RefreshChapters
import ca.gosyer.jui.domain.chapter.interactor.StopChapterDownload
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterFlags
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterBookmarked
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterMarkPreviousRead
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterRead
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.download.service.DownloadService
import ca.gosyer.jui.domain.library.interactor.AddMangaToLibrary
@@ -55,7 +57,9 @@ class MangaScreenViewModel @Inject constructor(
private val refreshManga: RefreshManga,
private val getChapters: GetChapters,
private val refreshChapters: RefreshChapters,
private val updateChapterFlags: UpdateChapterFlags,
private val updateChapterRead: UpdateChapterRead,
private val updateChapterBookmarked: UpdateChapterBookmarked,
private val updateChapterMarkPreviousRead: UpdateChapterMarkPreviousRead,
private val queueChapterDownload: QueueChapterDownload,
private val stopChapterDownload: StopChapterDownload,
private val deleteChapterDownload: DeleteChapterDownload,
@@ -222,7 +226,7 @@ class MangaScreenViewModel @Inject constructor(
val chapter = findChapter(index) ?: return
scope.launch {
manga.value.item?.let { manga ->
updateChapterFlags.await(manga, index, read = chapter.read.not())
updateChapterRead.await(manga, index, read = chapter.read.not())
refreshChaptersAsync(manga.id).await()
}
}
@@ -232,7 +236,7 @@ class MangaScreenViewModel @Inject constructor(
val chapter = findChapter(index) ?: return
scope.launch {
manga.value.item?.let { manga ->
updateChapterFlags.await(manga, index, bookmarked = chapter.bookmarked.not())
updateChapterBookmarked.await(manga, index, bookmarked = chapter.bookmarked.not())
refreshChaptersAsync(manga.id).await()
}
}
@@ -241,7 +245,7 @@ class MangaScreenViewModel @Inject constructor(
fun markPreviousRead(index: Int) {
scope.launch {
manga.value.item?.let { manga ->
updateChapterFlags.await(manga, index, markPreviousRead = true)
updateChapterMarkPreviousRead.await(manga, index)
refreshChaptersAsync(manga.id).await()
}
}

View File

@@ -11,8 +11,9 @@ import ca.gosyer.jui.core.prefs.getAsFlow
import ca.gosyer.jui.domain.chapter.interactor.GetChapter
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
import ca.gosyer.jui.domain.chapter.interactor.GetChapters
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterFlags
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterLastPageRead
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterMeta
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterRead
import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.manga.interactor.GetManga
import ca.gosyer.jui.domain.manga.interactor.UpdateMangaMeta
@@ -66,7 +67,8 @@ class ReaderMenuViewModel @Inject constructor(
private val getChapters: GetChapters,
private val getChapter: GetChapter,
private val getChapterPage: GetChapterPage,
private val updateChapterFlags: UpdateChapterFlags,
private val updateChapterRead: UpdateChapterRead,
private val updateChapterLastPageRead: UpdateChapterLastPageRead,
private val updateMangaMeta: UpdateMangaMeta,
private val updateChapterMeta: UpdateChapterMeta,
private val chapterCache: ChapterCache,
@@ -305,14 +307,14 @@ class ReaderMenuViewModel @Inject constructor(
}
private fun markChapterRead(chapter: ReaderChapter) {
scope.launch { updateChapterFlags.await(chapter.chapter, read = true) }
scope.launch { updateChapterRead.await(chapter.chapter, read = true) }
}
@OptIn(DelicateCoroutinesApi::class)
fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) {
chapter ?: return
if (chapter.read) return
GlobalScope.launch { updateChapterFlags.await(chapter, lastPageRead = lastPageRead) }
GlobalScope.launch { updateChapterLastPageRead.await(chapter, lastPageRead = lastPageRead) }
}
fun updateLastPageReadOffset(offset: Int) {

View File

@@ -6,13 +6,13 @@
package ca.gosyer.jui.ui.sources.browse
import ca.gosyer.jui.data.source.SourceRepositoryImpl
import ca.gosyer.jui.domain.library.model.DisplayMode
import ca.gosyer.jui.domain.library.service.LibraryPreferences
import ca.gosyer.jui.domain.manga.model.Manga
import ca.gosyer.jui.domain.source.model.MangaPage
import ca.gosyer.jui.domain.source.model.Source
import ca.gosyer.jui.domain.source.service.CatalogPreferences
import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.ui.base.model.StableHolder
import ca.gosyer.jui.uicore.vm.ContextWrapper
import ca.gosyer.jui.uicore.vm.ViewModel
@@ -32,7 +32,7 @@ import org.lighthousegames.logging.logging
class SourceScreenViewModel(
private val source: Source,
private val sourceHandler: SourceRepositoryImpl,
private val sourceHandler: SourceRepository,
private val catalogPreferences: CatalogPreferences,
private val libraryPreferences: LibraryPreferences,
contextWrapper: ContextWrapper,
@@ -40,7 +40,7 @@ class SourceScreenViewModel(
) : ViewModel(contextWrapper) {
@Inject constructor(
sourceHandler: SourceRepositoryImpl,
sourceHandler: SourceRepository,
catalogPreferences: CatalogPreferences,
libraryPreferences: LibraryPreferences,
contextWrapper: ContextWrapper,
@@ -128,7 +128,11 @@ class SourceScreenViewModel(
private suspend fun getPage(): MangaPage? {
return when {
isLatest.value -> sourceHandler.getLatestManga(source.id, pageNum.value)
_query.value != null || _usingFilters.value -> sourceHandler.getSearchResults(source.id, _query.value.orEmpty(), pageNum.value)
_query.value != null || _usingFilters.value -> sourceHandler.getSearchResults(
source.id,
_query.value?.ifBlank { null },
pageNum.value
)
else -> sourceHandler.getPopularManga(source.id, pageNum.value)
}
.catch {

View File

@@ -6,8 +6,9 @@
package ca.gosyer.jui.ui.sources.browse.filter
import ca.gosyer.jui.data.source.SourceRepositoryImpl
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilter
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilterChange
import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.ui.base.model.StableHolder
import ca.gosyer.jui.ui.sources.browse.filter.model.SourceFiltersView
import ca.gosyer.jui.uicore.vm.ContextWrapper
@@ -25,16 +26,18 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.supervisorScope
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class SourceFiltersViewModel(
private val sourceId: Long,
private val sourceHandler: SourceRepositoryImpl,
private val sourceHandler: SourceRepository,
contextWrapper: ContextWrapper
) : ViewModel(contextWrapper) {
@Inject constructor(
sourceHandler: SourceRepositoryImpl,
sourceHandler: SourceRepository,
contextWrapper: ContextWrapper,
params: Params
) : this(
@@ -69,11 +72,11 @@ class SourceFiltersViewModel(
.onEach {
sourceHandler.setFilter(
sourceId,
filter.index,
childFilter.index,
it
)
.collect()
SourceFilterChange(
filter.index,
Json.encodeToString(SourceFilterChange(childFilter.index, it))
)
).collect()
getFilters()
}
.launchIn(this)
@@ -81,7 +84,7 @@ class SourceFiltersViewModel(
} else {
filter.state.drop(1).filterNotNull()
.onEach {
sourceHandler.setFilter(sourceId, filter.index, it)
sourceHandler.setFilter(sourceId, SourceFilterChange(filter.index, it))
.collect()
getFilters()
}

View File

@@ -7,10 +7,10 @@
package ca.gosyer.jui.ui.sources.globalsearch
import androidx.compose.runtime.snapshots.SnapshotStateMap
import ca.gosyer.jui.data.source.SourceRepositoryImpl
import ca.gosyer.jui.domain.manga.model.Manga
import ca.gosyer.jui.domain.source.model.Source
import ca.gosyer.jui.domain.source.service.CatalogPreferences
import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.i18n.MR
import ca.gosyer.jui.ui.base.model.StableHolder
import ca.gosyer.jui.uicore.vm.ContextWrapper
@@ -40,7 +40,7 @@ import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class GlobalSearchViewModel @Inject constructor(
private val sourceHandler: SourceRepositoryImpl,
private val sourceHandler: SourceRepository,
catalogPreferences: CatalogPreferences,
contextWrapper: ContextWrapper,
params: Params

View File

@@ -9,9 +9,9 @@ package ca.gosyer.jui.ui.sources.home
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.intl.Locale
import ca.gosyer.jui.core.lang.displayName
import ca.gosyer.jui.data.source.SourceRepositoryImpl
import ca.gosyer.jui.domain.source.model.Source
import ca.gosyer.jui.domain.source.service.CatalogPreferences
import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.i18n.MR
import ca.gosyer.jui.uicore.vm.ContextWrapper
import ca.gosyer.jui.uicore.vm.ViewModel
@@ -32,7 +32,7 @@ import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class SourceHomeScreenViewModel @Inject constructor(
private val sourceHandler: SourceRepositoryImpl,
private val sourceHandler: SourceRepository,
catalogPreferences: CatalogPreferences,
contextWrapper: ContextWrapper
) : ViewModel(contextWrapper) {

View File

@@ -6,8 +6,9 @@
package ca.gosyer.jui.ui.sources.settings
import ca.gosyer.jui.data.source.SourceRepositoryImpl
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreferenceChange
import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.ui.base.model.StableHolder
import ca.gosyer.jui.ui.sources.settings.model.SourceSettingsView
import ca.gosyer.jui.uicore.vm.ContextWrapper
@@ -29,7 +30,7 @@ import me.tatarka.inject.annotations.Inject
import org.lighthousegames.logging.logging
class SourceSettingsScreenViewModel @Inject constructor(
private val sourceHandler: SourceRepositoryImpl,
private val sourceHandler: SourceRepository,
contextWrapper: ContextWrapper,
private val params: Params
) : ViewModel(contextWrapper) {
@@ -47,7 +48,7 @@ class SourceSettingsScreenViewModel @Inject constructor(
setting.state.drop(1)
.filterNotNull()
.onEach {
sourceHandler.setSourceSetting(params.sourceId, setting.index, it)
sourceHandler.setSourceSetting(params.sourceId, SourcePreferenceChange(setting.index, it))
.catch {
log.warn(it) { "Error setting source setting" }
}