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.websockets)
implementation(libs.ktor.auth) implementation(libs.ktor.auth)
// Ktorfit
implementation(libs.ktorfit.lib)
ksp(libs.ktorfit.ksp)
// Logging // Logging
implementation(libs.logging.kmlogging) 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.model.DownloadStatus
import ca.gosyer.jui.domain.download.service.DownloadService import ca.gosyer.jui.domain.download.service.DownloadService
import ca.gosyer.jui.domain.download.service.DownloadService.Companion.status 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 ca.gosyer.jui.i18n.MR
import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.resources.desc.desc
import dev.icerock.moko.resources.format import dev.icerock.moko.resources.format
@@ -146,7 +145,7 @@ class AndroidDownloadService : Service() {
client.ws( client.ws(
host = serverUrl.host, host = serverUrl.host,
port = serverUrl.port, port = serverUrl.port,
path = serverUrl.encodedPath + downloadsQuery() path = serverUrl.encodedPath + "/api/v1/downloads"
) { ) {
errorConnectionCount = 0 errorConnectionCount = 0
status.value = Status.RUNNING status.value = Status.RUNNING

View File

@@ -17,3 +17,10 @@ fun String.chop(count: Int, replacement: String = "…"): String {
this 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 package ca.gosyer.jui.data
import ca.gosyer.jui.data.backup.BackupRepositoryImpl import ca.gosyer.jui.core.lang.addSuffix
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.domain.backup.service.BackupRepository import ca.gosyer.jui.domain.backup.service.BackupRepository
import ca.gosyer.jui.domain.category.service.CategoryRepository import ca.gosyer.jui.domain.category.service.CategoryRepository
import ca.gosyer.jui.domain.chapter.service.ChapterRepository 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.download.service.DownloadRepository
import ca.gosyer.jui.domain.extension.service.ExtensionRepository import ca.gosyer.jui.domain.extension.service.ExtensionRepository
import ca.gosyer.jui.domain.library.service.LibraryRepository import ca.gosyer.jui.domain.library.service.LibraryRepository
import ca.gosyer.jui.domain.manga.service.MangaRepository 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.settings.service.SettingsRepository
import ca.gosyer.jui.domain.source.service.SourceRepository import ca.gosyer.jui.domain.source.service.SourceRepository
import ca.gosyer.jui.domain.updates.service.UpdatesRepository import ca.gosyer.jui.domain.updates.service.UpdatesRepository
import de.jensklingenberg.ktorfit.Ktorfit
import me.tatarka.inject.annotations.Provides import me.tatarka.inject.annotations.Provides
interface DataComponent { interface DataComponent {
val BackupRepositoryImpl.bind: BackupRepository @Provides
@Provides get() = this fun ktorfit(http: Http, serverPreferences: ServerPreferences) = Ktorfit
.Builder()
.httpClient(http)
.requestConverter(FlowIORequestConverter())
.baseUrl(serverPreferences.serverUrl().get().toString().addSuffix('/'))
.build()
val CategoryRepositoryImpl.bind: CategoryRepository @Provides
@Provides get() = this fun backupRepository(ktorfit: Ktorfit) = ktorfit.createIt<BackupRepository>()
val ChapterRepositoryImpl.bind: ChapterRepository @Provides
@Provides get() = this fun categoryRepository(ktorfit: Ktorfit) = ktorfit.createIt<CategoryRepository>()
val DownloadRepositoryImpl.bind: DownloadRepository @Provides
@Provides get() = this fun chapterRepository(ktorfit: Ktorfit) = ktorfit.createIt<ChapterRepository>()
val ExtensionRepositoryImpl.bind: ExtensionRepository @Provides
@Provides get() = this fun downloadRepository(ktorfit: Ktorfit) = ktorfit.createIt<DownloadRepository>()
val LibraryRepositoryImpl.bind: LibraryRepository @Provides
@Provides get() = this fun extensionRepository(ktorfit: Ktorfit) = ktorfit.createIt<ExtensionRepository>()
val MangaRepositoryImpl.bind: MangaRepository @Provides
@Provides get() = this fun libraryRepository(ktorfit: Ktorfit) = ktorfit.createIt<LibraryRepository>()
val SettingsRepositoryImpl.bind: SettingsRepository @Provides
@Provides get() = this fun mangaRepository(ktorfit: Ktorfit) = ktorfit.createIt<MangaRepository>()
val SourceRepositoryImpl.bind: SourceRepository @Provides
@Provides get() = this fun settingsRepository(ktorfit: Ktorfit) = ktorfit.createIt<SettingsRepository>()
val UpdatesRepositoryImpl.bind: UpdatesRepository @Provides
@Provides get() = this 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.websockets)
implementation(libs.ktor.auth) implementation(libs.ktor.auth)
// Ktorfit
implementation(libs.ktorfit.lib)
ksp(libs.ktorfit.ksp)
// Logging // Logging
implementation(libs.logging.slf4j.api) implementation(libs.logging.slf4j.api)
implementation(libs.logging.slf4j.jul) implementation(libs.logging.slf4j.jul)

View File

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

View File

@@ -4,14 +4,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 inline fun <reified T> Ktorfit.createIt(): T = create()
annotation class Delete
annotation class Patch
annotation class WS

View File

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

View File

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

View File

@@ -6,14 +6,55 @@
package ca.gosyer.jui.domain.backup.service package ca.gosyer.jui.domain.backup.service
import ca.gosyer.jui.core.io.SYSTEM
import ca.gosyer.jui.domain.backup.model.BackupValidationResult 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.HttpRequestBuilder
import io.ktor.client.request.forms.formData
import io.ktor.client.statement.HttpResponse 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 kotlinx.coroutines.flow.Flow
import okio.FileSystem
import okio.Path import okio.Path
import okio.buffer
interface BackupRepository { interface BackupRepository {
fun importBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}): Flow<HttpResponse>
fun validateBackupFile(file: Path, block: HttpRequestBuilder.() -> Unit = {}): Flow<BackupValidationResult> @Multipart
fun exportBackupFile(block: HttpRequestBuilder.() -> Unit = {}): Flow<HttpResponse> @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 ca.gosyer.jui.domain.category.service.CategoryRepository
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.singleOrNull 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
@@ -18,7 +19,12 @@ class GetCategories @Inject constructor(private val categoryRepository: Category
.catch { log.warn(it) { "Failed to get categories" } } .catch { log.warn(it) { "Failed to get categories" } }
.singleOrNull() .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 { companion object {
private val log = logging() private val log = logging()

View File

@@ -15,28 +15,24 @@ import org.lighthousegames.logging.logging
class ModifyCategory @Inject constructor(private val categoryRepository: CategoryRepository) { 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, categoryId = categoryId,
name = name, name = name
isLanding = isLanding ).catch { log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } }.collect()
).catch { log.warn(it) { "Failed to modify category $categoryId with options: name=$name,isLanding=$isLanding" } }.collect()
suspend fun await(category: Category, name: String? = null, isLanding: Boolean? = null) = asFlow( suspend fun await(category: Category, name: String? = null) = asFlow(
category = category, category = category,
name = name, name = name
isLanding = isLanding ).catch { log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } }.collect()
).catch { log.warn(it) { "Failed to modify category ${category.name} with options: name=$name,isLanding=$isLanding" } }.collect()
fun asFlow(categoryId: Long, name: String? = null, isLanding: Boolean? = null) = categoryRepository.modifyCategory( fun asFlow(categoryId: Long, name: String) = categoryRepository.modifyCategory(
categoryId = categoryId, categoryId = categoryId,
name = name, name = name
isLanding = isLanding
) )
fun asFlow(category: Category, name: String? = null, isLanding: Boolean? = null) = categoryRepository.modifyCategory( fun asFlow(category: Category, name: String? = null) = categoryRepository.modifyCategory(
categoryId = category.id, categoryId = category.id,
name = name, name = name ?: category.name
isLanding = isLanding
) )
companion object { 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.category.model.Category
import ca.gosyer.jui.domain.manga.model.Manga 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 io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface CategoryRepository { interface CategoryRepository {
fun getMangaCategories(mangaId: Long): Flow<List<Category>> @GET("api/v1/manga/{mangaId}/category/")
fun addMangaToCategory(mangaId: Long, categoryId: Long): Flow<HttpResponse> fun getMangaCategories(
fun removeMangaFromCategory(mangaId: Long, categoryId: Long): Flow<HttpResponse> @Path("mangaId") mangaId: Long
fun getCategories(dropDefault: Boolean = false): Flow<List<Category>> ): Flow<List<Category>>
fun createCategory(name: String): Flow<HttpResponse>
fun modifyCategory(categoryId: Long, name: String? = null, isLanding: Boolean? = null): Flow<HttpResponse> @GET("api/v1/manga/{mangaId}/category/{categoryId}")
fun reorderCategory(to: Int, from: Int): Flow<HttpResponse> fun addMangaToCategory(
fun deleteCategory(categoryId: Long): Flow<HttpResponse> @Path("mangaId") mangaId: Long,
fun getMangaFromCategory(categoryId: Long): Flow<List<Manga>> @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 package ca.gosyer.jui.domain.chapter.service
import ca.gosyer.jui.domain.chapter.model.Chapter 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.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ChapterRepository { 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( fun updateChapter(
mangaId: Long, @Path("mangaId") mangaId: Long,
chapterIndex: Int, @Path("chapterIndex") chapterIndex: Int,
read: Boolean? = null, @Field("read") read: Boolean? = null,
bookmarked: Boolean? = null, @Field("bookmarked") bookmarked: Boolean? = null,
lastPageRead: Int? = null, @Field("lastPageRead") lastPageRead: Int? = null,
markPreviousRead: Boolean? = 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> ): 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( fun getPage(
mangaId: Long, @Path("mangaId") mangaId: Long,
chapterIndex: Int, @Path("chapterIndex") chapterIndex: Int,
pageNum: Int, @Path("pageNum") pageNum: Int,
block: HttpRequestBuilder.() -> Unit @ReqBuilder block: HttpRequestBuilder.() -> Unit
): Flow<HttpResponse> ): Flow<HttpResponse>
fun deleteChapterDownload(mangaId: Long, chapterIndex: Int): Flow<HttpResponse> @DELETE("api/v1/manga/{mangaId}/chapter/{chapterIndex}")
fun queueChapterDownload(mangaId: Long, chapterIndex: Int): Flow<HttpResponse> fun deleteChapterDownload(
fun stopChapterDownload(mangaId: Long, chapterIndex: Int): Flow<HttpResponse> @Path("mangaId") mangaId: Long,
fun updateChapterMeta(mangaId: Long, chapterIndex: Int, key: String, value: String): Flow<HttpResponse> @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 package ca.gosyer.jui.domain.download.service
import de.jensklingenberg.ktorfit.http.GET
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface DownloadRepository { interface DownloadRepository {
@GET("api/v1/downloads/start")
fun startDownloading(): Flow<HttpResponse> fun startDownloading(): Flow<HttpResponse>
@GET("api/v1/downloads/stop")
fun stopDownloading(): Flow<HttpResponse> fun stopDownloading(): Flow<HttpResponse>
@GET("api/v1/downloads/clear")
fun clearDownloadQueue(): Flow<HttpResponse> 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.DownloadStatus
import ca.gosyer.jui.domain.download.model.DownloaderStatus import ca.gosyer.jui.domain.download.model.DownloaderStatus
import ca.gosyer.jui.domain.server.Http 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 ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
@@ -28,7 +27,7 @@ class DownloadService @Inject constructor(
get() = status get() = status
override val query: String override val query: String
get() = downloadsQuery() 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())

View File

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

View File

@@ -7,15 +7,36 @@
package ca.gosyer.jui.domain.extension.service package ca.gosyer.jui.domain.extension.service
import ca.gosyer.jui.domain.extension.model.Extension 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.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ExtensionRepository { interface ExtensionRepository {
@GET("api/v1/extension/list")
fun getExtensionList(): Flow<List<Extension>> fun getExtensionList(): Flow<List<Extension>>
fun installExtension(extension: Extension): Flow<HttpResponse>
fun updateExtension(extension: Extension): Flow<HttpResponse> @GET("api/v1/extension/install/{pkgName}")
fun uninstallExtension(extension: Extension): Flow<HttpResponse> fun installExtension(
fun getApkIcon(extension: Extension, block: HttpRequestBuilder.() -> Unit): Flow<ByteReadChannel> @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 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 io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface LibraryRepository { 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.base.WebsocketService
import ca.gosyer.jui.domain.library.model.UpdateStatus import ca.gosyer.jui.domain.library.model.UpdateStatus
import ca.gosyer.jui.domain.server.Http 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 ca.gosyer.jui.domain.server.service.ServerPreferences
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
@@ -26,7 +25,7 @@ class LibraryUpdateService @Inject constructor(
override val _status: MutableStateFlow<Status> = MutableStateFlow(Status.STARTING) override val _status: MutableStateFlow<Status> = MutableStateFlow(Status.STARTING)
override val query: String override val query: String
get() = updatesQuery() get() = "/api/v1/update"
override suspend fun onReceived(frame: Frame.Text) { override suspend fun onReceived(frame: Frame.Text) {
val status = json.decodeFromString<UpdateStatus>(frame.readText()) val status = json.decodeFromString<UpdateStatus>(frame.readText())

View File

@@ -7,13 +7,36 @@
package ca.gosyer.jui.domain.manga.service package ca.gosyer.jui.domain.manga.service
import ca.gosyer.jui.domain.manga.model.Manga 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.request.HttpRequestBuilder
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface MangaRepository { interface MangaRepository {
fun getManga(mangaId: Long, refresh: Boolean = false): Flow<Manga> @GET("api/v1/manga/{mangaId}/")
fun getMangaThumbnail(mangaId: Long, block: HttpRequestBuilder.() -> Unit): Flow<ByteReadChannel> fun getManga(
fun updateMangaMeta(mangaId: Long, key: String, value: String): Flow<HttpResponse> @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 package ca.gosyer.jui.domain.settings.service
import ca.gosyer.jui.domain.settings.model.About 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 io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {
@GET("api/v1/settings/about")
fun aboutServer(): Flow<About> fun aboutServer(): Flow<About>
@POST("api/v1/settings/check-update")
fun checkUpdate(): Flow<HttpResponse> 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.sourcefilters.SourceFilterChange
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreferenceChange 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 io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SourceRepository { interface SourceRepository {
@GET("api/v1/source/list")
fun getSourceList(): Flow<List<Source>> fun getSourceList(): Flow<List<Source>>
fun getSourceInfo(sourceId: Long): Flow<Source>
fun getPopularManga(sourceId: Long, pageNum: Int): Flow<MangaPage> @GET("api/v1/source/{sourceId}")
fun getLatestManga(sourceId: Long, pageNum: Int): Flow<MangaPage> fun getSourceInfo(
fun getSearchResults(sourceId: Long, searchTerm: String, pageNum: Int): Flow<MangaPage> @Path("sourceId") sourceId: Long
fun getFilterList(sourceId: Long, reset: Boolean = false): Flow<List<SourceFilter>> ): Flow<Source>
fun setFilter(sourceId: Long, sourceFilter: SourceFilterChange): Flow<HttpResponse>
fun setFilter(sourceId: Long, position: Int, value: Any): Flow<HttpResponse> @GET("api/v1/source/{sourceId}/popular/{pageNum}")
fun setFilter(sourceId: Long, parentPosition: Int, childPosition: Int, value: Any): Flow<HttpResponse> fun getPopularManga(
fun getSourceSettings(sourceId: Long): Flow<List<SourcePreference>> @Path("sourceId") sourceId: Long,
fun setSourceSetting(sourceId: Long, sourcePreference: SourcePreferenceChange): Flow<HttpResponse> @Path("pageNum") pageNum: Int
fun setSourceSetting(sourceId: Long, position: Int, value: Any): Flow<HttpResponse> ): 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 package ca.gosyer.jui.domain.updates.service
import ca.gosyer.jui.domain.updates.model.Updates 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 io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface UpdatesRepository { 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 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 # Network
ktor = "2.1.1" ktor = "2.1.1"
ktorfit = "1.0.0-beta14"
# Logging # Logging
slf4j = "1.7.36" 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-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-websockets = { module = "io.ktor:ktor-client-websockets", 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" } 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
logging-slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } 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.QueueChapterDownload
import ca.gosyer.jui.domain.chapter.interactor.RefreshChapters import ca.gosyer.jui.domain.chapter.interactor.RefreshChapters
import ca.gosyer.jui.domain.chapter.interactor.StopChapterDownload 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.chapter.model.Chapter
import ca.gosyer.jui.domain.download.service.DownloadService import ca.gosyer.jui.domain.download.service.DownloadService
import ca.gosyer.jui.domain.library.interactor.AddMangaToLibrary import ca.gosyer.jui.domain.library.interactor.AddMangaToLibrary
@@ -55,7 +57,9 @@ class MangaScreenViewModel @Inject constructor(
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 updateChapterFlags: UpdateChapterFlags, private val updateChapterRead: UpdateChapterRead,
private val updateChapterBookmarked: UpdateChapterBookmarked,
private val updateChapterMarkPreviousRead: UpdateChapterMarkPreviousRead,
private val queueChapterDownload: QueueChapterDownload, private val queueChapterDownload: QueueChapterDownload,
private val stopChapterDownload: StopChapterDownload, private val stopChapterDownload: StopChapterDownload,
private val deleteChapterDownload: DeleteChapterDownload, private val deleteChapterDownload: DeleteChapterDownload,
@@ -222,7 +226,7 @@ class MangaScreenViewModel @Inject constructor(
val chapter = findChapter(index) ?: return val chapter = findChapter(index) ?: return
scope.launch { scope.launch {
manga.value.item?.let { manga -> 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() refreshChaptersAsync(manga.id).await()
} }
} }
@@ -232,7 +236,7 @@ class MangaScreenViewModel @Inject constructor(
val chapter = findChapter(index) ?: return val chapter = findChapter(index) ?: return
scope.launch { scope.launch {
manga.value.item?.let { manga -> 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() refreshChaptersAsync(manga.id).await()
} }
} }
@@ -241,7 +245,7 @@ class MangaScreenViewModel @Inject constructor(
fun markPreviousRead(index: Int) { fun markPreviousRead(index: Int) {
scope.launch { scope.launch {
manga.value.item?.let { manga -> manga.value.item?.let { manga ->
updateChapterFlags.await(manga, index, markPreviousRead = true) updateChapterMarkPreviousRead.await(manga, index)
refreshChaptersAsync(manga.id).await() 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.GetChapter
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
import ca.gosyer.jui.domain.chapter.interactor.GetChapters 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.UpdateChapterMeta
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterRead
import ca.gosyer.jui.domain.chapter.model.Chapter import ca.gosyer.jui.domain.chapter.model.Chapter
import ca.gosyer.jui.domain.manga.interactor.GetManga import ca.gosyer.jui.domain.manga.interactor.GetManga
import ca.gosyer.jui.domain.manga.interactor.UpdateMangaMeta import ca.gosyer.jui.domain.manga.interactor.UpdateMangaMeta
@@ -66,7 +67,8 @@ class ReaderMenuViewModel @Inject constructor(
private val getChapters: GetChapters, private val getChapters: GetChapters,
private val getChapter: GetChapter, private val getChapter: GetChapter,
private val getChapterPage: GetChapterPage, private val getChapterPage: GetChapterPage,
private val updateChapterFlags: UpdateChapterFlags, private val updateChapterRead: UpdateChapterRead,
private val updateChapterLastPageRead: UpdateChapterLastPageRead,
private val updateMangaMeta: UpdateMangaMeta, private val updateMangaMeta: UpdateMangaMeta,
private val updateChapterMeta: UpdateChapterMeta, private val updateChapterMeta: UpdateChapterMeta,
private val chapterCache: ChapterCache, private val chapterCache: ChapterCache,
@@ -305,14 +307,14 @@ class ReaderMenuViewModel @Inject constructor(
} }
private fun markChapterRead(chapter: ReaderChapter) { private fun markChapterRead(chapter: ReaderChapter) {
scope.launch { updateChapterFlags.await(chapter.chapter, read = true) } scope.launch { updateChapterRead.await(chapter.chapter, read = true) }
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) { fun sendProgress(chapter: Chapter? = this.chapter.value?.chapter, lastPageRead: Int = currentPage.value) {
chapter ?: return chapter ?: return
if (chapter.read) return if (chapter.read) return
GlobalScope.launch { updateChapterFlags.await(chapter, lastPageRead = lastPageRead) } GlobalScope.launch { updateChapterLastPageRead.await(chapter, lastPageRead = lastPageRead) }
} }
fun updateLastPageReadOffset(offset: Int) { fun updateLastPageReadOffset(offset: Int) {

View File

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

View File

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

View File

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

View File

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

View File

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