diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt b/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt index a3678252..a13a8fff 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt @@ -23,17 +23,12 @@ import ca.gosyer.jui.core.prefs.getAsFlow import ca.gosyer.jui.domain.base.WebsocketService.Actions import ca.gosyer.jui.domain.base.WebsocketService.Status import ca.gosyer.jui.domain.download.model.DownloadState -import ca.gosyer.jui.domain.download.model.DownloadStatus import ca.gosyer.jui.domain.download.service.DownloadService import ca.gosyer.jui.domain.download.service.DownloadService.Companion.status import ca.gosyer.jui.i18n.MR import com.diamondedge.logging.logging import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.resources.format -import io.ktor.client.plugins.websocket.ws -import io.ktor.http.URLProtocol -import io.ktor.websocket.Frame -import io.ktor.websocket.readText import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -42,13 +37,9 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.job import kotlinx.serialization.json.Json import java.util.regex.Pattern @@ -154,31 +145,12 @@ class AndroidDownloadService : Service() { throw CancellationException() } runCatching { - client.ws( - host = serverUrl.host, - port = serverUrl.port, - path = serverUrl.encodedPath + "/api/v1/downloads", - request = { - if (serverUrl.protocol == URLProtocol.HTTPS) { - url.protocol = URLProtocol.WSS - } - }, - ) { - errorConnectionCount = 0 - status.value = Status.RUNNING - send(Frame.Text("STATUS")) - - incoming.receiveAsFlow() - .filterIsInstance() - .map { json.decodeFromString(it.readText()) } - .distinctUntilChanged() - .drop(1) - .mapLatest(::onReceived) - .catch { - log.warn(it) { "Error running downloader" } - } - .collect() - } + appComponent.downloadService + .getSubscription() + .onEach { + onReceived() + } + .collect() }.throwIfCancellation().isFailure.let { status.value = Status.STARTING if (it) errorConnectionCount++ @@ -193,13 +165,11 @@ class AndroidDownloadService : Service() { .launchIn(ioScope) } - private fun onReceived(status: DownloadStatus) { - DownloadService.downloaderStatus.value = status.status - DownloadService.downloadQueue.value = status.queue - val downloadingChapter = status.queue.lastOrNull { it.state == DownloadState.Downloading } + private fun onReceived() { + val downloadingChapter = DownloadService.downloadQueue.value.lastOrNull { it.state == DownloadState.DOWNLOADING } if (downloadingChapter != null) { val notification = with(progressNotificationBuilder) { - val max = downloadingChapter.chapter.pageCount ?: 0 + val max = downloadingChapter.chapter.pageCount val current = (max * downloadingChapter.progress).toInt().coerceIn(0, max) setProgress(max, current, false) diff --git a/data/src/commonMain/graphql/Download.graphql b/data/src/commonMain/graphql/Download.graphql index f8977580..91ec24d1 100644 --- a/data/src/commonMain/graphql/Download.graphql +++ b/data/src/commonMain/graphql/Download.graphql @@ -39,3 +39,29 @@ mutation EnqueueChapterDownloads($ids: [Int!]!) { clientMutationId } } + +subscription DownloadStatusChanged($maxUpdates: Int = 30) { + downloadStatusChanged(input: { maxUpdates: $maxUpdates }) { + initial { + ...DownloadFragment + } + updates { + type + download { + ...DownloadFragment + } + } + state + omittedUpdates + } +} + + +query DownloadStatus { + downloadStatus { + state + queue { + ...DownloadFragment + } + } +} diff --git a/data/src/commonMain/graphql/fragments/DownloadFragments.graphql b/data/src/commonMain/graphql/fragments/DownloadFragments.graphql new file mode 100644 index 00000000..b00890e1 --- /dev/null +++ b/data/src/commonMain/graphql/fragments/DownloadFragments.graphql @@ -0,0 +1,17 @@ +fragment DownloadFragment on DownloadType { + chapter { + id + name + pageCount + } + manga { + id + title + thumbnailUrl + thumbnailUrlLastFetched + } + position + progress + state + tries +} diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt index 9b12f94f..3127007f 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.data +import ca.gosyer.jui.core.di.AppScope import ca.gosyer.jui.data.backup.BackupRepositoryImpl import ca.gosyer.jui.data.category.CategoryRepositoryImpl import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl @@ -31,100 +32,125 @@ import ca.gosyer.jui.domain.settings.service.SettingsRepository import ca.gosyer.jui.domain.source.service.SourceRepository import ca.gosyer.jui.domain.updates.service.UpdatesRepository import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.annotations.ApolloExperimental import com.apollographql.apollo.network.ws.GraphQLWsProtocol import com.apollographql.ktor.ktorClient +import io.ktor.client.HttpClient import io.ktor.http.URLBuilder +import io.ktor.http.Url import io.ktor.http.appendPathSegments import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import me.tatarka.inject.annotations.Provides -interface DataComponent : SharedDataComponent { - @Provides - fun apolloClient( - http: Http, - serverPreferences: ServerPreferences, - ) = ApolloClient.Builder() - .serverUrl( - URLBuilder(serverPreferences.serverUrl().get()) - .appendPathSegments("api", "graphql") - .buildString(), - ) - .ktorClient(http) - .wsProtocol(GraphQLWsProtocol.Factory()) +typealias ApolloAppClient = StateFlow + +private fun getApolloClient( + httpClient: HttpClient, + serverUrl: Url, +): ApolloClient { + val url = URLBuilder(serverUrl) + .appendPathSegments("api", "graphql") + .buildString() + return ApolloClient.Builder() + .serverUrl(url) + .ktorClient(httpClient) + .wsProtocol(GraphQLWsProtocol.Factory(pingIntervalMillis = 30)) .dispatcher(Dispatchers.IO) .build() +} + +interface DataComponent : SharedDataComponent { + @OptIn(ApolloExperimental::class) + @Provides + @AppScope + fun apolloAppClient( + http: Http, + serverPreferences: ServerPreferences, + ): ApolloAppClient = + http + .map { getApolloClient(it, serverPreferences.serverUrl().get()) } + .stateIn( + GlobalScope, + SharingStarted.Eagerly, + getApolloClient(http.value, serverPreferences.serverUrl().get()), + ) @Provides - fun settingsRepository(apolloClient: ApolloClient): SettingsRepository = SettingsRepositoryImpl(apolloClient) + fun settingsRepository(apolloAppClient: ApolloAppClient): SettingsRepository = SettingsRepositoryImpl(apolloAppClient) @Provides fun categoryRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): CategoryRepository = CategoryRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): CategoryRepository = CategoryRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun chapterRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): ChapterRepository = ChapterRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): ChapterRepository = ChapterRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun downloadRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): DownloadRepository = DownloadRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): DownloadRepository = DownloadRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun extensionRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): ExtensionRepository = ExtensionRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): ExtensionRepository = ExtensionRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun globalRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): GlobalRepository = GlobalRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): GlobalRepository = GlobalRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun libraryRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): LibraryRepository = LibraryRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): LibraryRepository = LibraryRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun mangaRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): MangaRepository = MangaRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): MangaRepository = MangaRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun sourceRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): SourceRepository = SourceRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): SourceRepository = SourceRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun updatesRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): UpdatesRepository = UpdatesRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): UpdatesRepository = UpdatesRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) @Provides fun backupRepository( - apolloClient: ApolloClient, + apolloAppClient: ApolloAppClient, http: Http, serverPreferences: ServerPreferences, - ): BackupRepository = BackupRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get()) + ): BackupRepository = BackupRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get()) } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt index 865156e1..c02dc90c 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/backup/BackupRepositoryImpl.kt @@ -1,6 +1,7 @@ package ca.gosyer.jui.data.backup import ca.gosyer.jui.core.io.toSource +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.CreateBackupMutation import ca.gosyer.jui.data.graphql.RestoreBackupMutation import ca.gosyer.jui.data.graphql.RestoreStatusQuery @@ -24,10 +25,13 @@ import okio.Source import okio.buffer class BackupRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : BackupRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun validateBackup(source: Source): Flow = apolloClient.query( ValidateBackupQuery( @@ -89,7 +93,7 @@ class BackupRepositoryImpl( .toFlow() .map { val url = it.dataAssertNoErrors.createBackup.url - val response = http.get( + val response = http.value.get( Url("$serverUrl$url"), ) val fileName = response.headers["content-disposition"]!! diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/category/CategoryRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/category/CategoryRepositoryImpl.kt index e378c123..0468ae89 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/category/CategoryRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/category/CategoryRepositoryImpl.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.data.category +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.AddMangaToCategoriesMutation import ca.gosyer.jui.data.graphql.CreateCategoryMutation import ca.gosyer.jui.data.graphql.DeleteCategoryMutation @@ -29,10 +30,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class CategoryRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : CategoryRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getMangaCategories(mangaId: Long): Flow> = apolloClient.query( GetMangaCategoriesQuery(mangaId.toInt()), diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/chapter/ChapterRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/chapter/ChapterRepositoryImpl.kt index 87abea20..2bbf9e2d 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/chapter/ChapterRepositoryImpl.kt @@ -1,5 +1,6 @@ package ca.gosyer.jui.data.chapter +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.DeleteDownloadedChapterMutation import ca.gosyer.jui.data.graphql.DeleteDownloadedChaptersMutation import ca.gosyer.jui.data.graphql.FetchChapterPagesMutation @@ -29,10 +30,13 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class ChapterRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : ChapterRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getChapter(chapterId: Long): Flow = apolloClient.query( GetChapterQuery(chapterId.toInt()), @@ -164,7 +168,7 @@ class ChapterRepositoryImpl( ): Flow { val realUrl = Url("$serverUrl$url") - return flow { emit(http.get(realUrl, block).readRawBytes()) } + return flow { emit(http.value.get(realUrl, block).readRawBytes()) } } companion object { diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/download/DownloadRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/download/DownloadRepositoryImpl.kt index ece04354..2f6371bb 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/download/DownloadRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/download/DownloadRepositoryImpl.kt @@ -6,25 +6,44 @@ package ca.gosyer.jui.data.download +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.ClearDownloaderMutation import ca.gosyer.jui.data.graphql.DequeueChapterDownloadMutation +import ca.gosyer.jui.data.graphql.DownloadStatusChangedSubscription +import ca.gosyer.jui.data.graphql.DownloadStatusQuery import ca.gosyer.jui.data.graphql.EnqueueChapterDownloadMutation import ca.gosyer.jui.data.graphql.EnqueueChapterDownloadsMutation import ca.gosyer.jui.data.graphql.ReorderChapterDownloadMutation import ca.gosyer.jui.data.graphql.StartDownloaderMutation import ca.gosyer.jui.data.graphql.StopDownloaderMutation +import ca.gosyer.jui.data.graphql.fragment.DownloadFragment +import ca.gosyer.jui.domain.download.model.DownloadChapter +import ca.gosyer.jui.domain.download.model.DownloadManga +import ca.gosyer.jui.domain.download.model.DownloadQueueItem +import ca.gosyer.jui.domain.download.model.DownloadState +import ca.gosyer.jui.domain.download.model.DownloadStatus +import ca.gosyer.jui.domain.download.model.DownloadUpdate +import ca.gosyer.jui.domain.download.model.DownloadUpdateType +import ca.gosyer.jui.domain.download.model.DownloadUpdates +import ca.gosyer.jui.domain.download.model.DownloaderState import ca.gosyer.jui.domain.download.service.DownloadRepository import ca.gosyer.jui.domain.server.Http import com.apollographql.apollo.ApolloClient import io.ktor.http.Url import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import ca.gosyer.jui.data.graphql.type.DownloadState as GraphQLDownloadState +import ca.gosyer.jui.data.graphql.type.DownloadUpdateType as GraphQLDownloadUpdateType +import ca.gosyer.jui.data.graphql.type.DownloaderState as GraphQLDownloaderState class DownloadRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : DownloadRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun startDownloading(): Flow = apolloClient.mutation( StartDownloaderMutation(), @@ -90,4 +109,82 @@ class DownloadRepositoryImpl( .map { it.dataAssertNoErrors } + + override fun downloadSubscription(): Flow = + apolloClient.subscription( + DownloadStatusChangedSubscription(), + ) + .toFlow() + .map { + val data = it.dataAssertNoErrors.downloadStatusChanged + + DownloadUpdates( + data.initial?.map { it.downloadFragment.toDownloadQueueItem() }, + data.omittedUpdates, + data.state.toClient(), + data.updates.map { + DownloadUpdate( + when (it.type) { + GraphQLDownloadUpdateType.QUEUED -> DownloadUpdateType.QUEUED + GraphQLDownloadUpdateType.DEQUEUED -> DownloadUpdateType.DEQUEUED + GraphQLDownloadUpdateType.PAUSED -> DownloadUpdateType.PAUSED + GraphQLDownloadUpdateType.STOPPED -> DownloadUpdateType.STOPPED + GraphQLDownloadUpdateType.PROGRESS -> DownloadUpdateType.PROGRESS + GraphQLDownloadUpdateType.FINISHED -> DownloadUpdateType.FINISHED + GraphQLDownloadUpdateType.ERROR -> DownloadUpdateType.ERROR + GraphQLDownloadUpdateType.POSITION -> DownloadUpdateType.POSITION + GraphQLDownloadUpdateType.UNKNOWN__ -> null + }, + it.download.downloadFragment.toDownloadQueueItem(), + ) + }, + ) + } + + override fun downloadStatus(): Flow = + apolloClient.query( + DownloadStatusQuery(), + ) + .toFlow() + .map { + val data = it.dataAssertNoErrors.downloadStatus + DownloadStatus( + data.state.toClient(), + data.queue.map { it.downloadFragment.toDownloadQueueItem() }, + ) + } + + companion object { + fun DownloadFragment.toDownloadQueueItem(): DownloadQueueItem = + DownloadQueueItem( + position = this.position, + progress = this.progress.toFloat(), + state = when (this.state) { + GraphQLDownloadState.QUEUED -> DownloadState.QUEUED + GraphQLDownloadState.DOWNLOADING -> DownloadState.DOWNLOADING + GraphQLDownloadState.FINISHED -> DownloadState.FINISHED + GraphQLDownloadState.ERROR -> DownloadState.ERROR + GraphQLDownloadState.UNKNOWN__ -> DownloadState.ERROR + }, + tries = this.tries, + chapter = DownloadChapter( + chapter.id.toLong(), + chapter.name, + chapter.pageCount, + ), + manga = DownloadManga( + manga.id.toLong(), + manga.title, + manga.thumbnailUrl, + manga.thumbnailUrlLastFetched ?: 0, + ), + ) + + fun GraphQLDownloaderState.toClient(): DownloaderState = + when (this) { + GraphQLDownloaderState.STARTED -> DownloaderState.STARTED + GraphQLDownloaderState.STOPPED -> DownloaderState.STOPPED + GraphQLDownloaderState.UNKNOWN__ -> DownloaderState.STOPPED + } + } } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/extension/ExtensionRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/extension/ExtensionRepositoryImpl.kt index c12df192..d2e718c4 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/extension/ExtensionRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/extension/ExtensionRepositoryImpl.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.data.extension +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.FetchExtensionsMutation import ca.gosyer.jui.data.graphql.InstallExtensionMutation import ca.gosyer.jui.data.graphql.InstallExternalExtensionMutation @@ -23,10 +24,13 @@ import kotlinx.coroutines.flow.map import okio.Source class ExtensionRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : ExtensionRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getExtensionList(): Flow> = apolloClient.mutation( FetchExtensionsMutation(), diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/global/GlobalRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/global/GlobalRepositoryImpl.kt index 0c9139c6..addc22a8 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/global/GlobalRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/global/GlobalRepositoryImpl.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.data.global +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.GetGlobalMetaQuery import ca.gosyer.jui.data.graphql.SetGlobalMetaMutation import ca.gosyer.jui.domain.global.model.GlobalMeta @@ -17,10 +18,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class GlobalRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : GlobalRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getGlobalMeta(): Flow = apolloClient.query( GetGlobalMetaQuery(), diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryRepositoryImpl.kt index 15ddda67..7e7647fc 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/library/LibraryRepositoryImpl.kt @@ -1,5 +1,6 @@ package ca.gosyer.jui.data.library +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.SetMangaInLibraryMutation import ca.gosyer.jui.domain.library.service.LibraryRepository import ca.gosyer.jui.domain.server.Http @@ -9,10 +10,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class LibraryRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : LibraryRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + fun setMangaInLibrary( mangaId: Long, inLibrary: Boolean, diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/manga/MangaRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/manga/MangaRepositoryImpl.kt index 7f50c25c..4d5879fb 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/manga/MangaRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/manga/MangaRepositoryImpl.kt @@ -1,5 +1,6 @@ package ca.gosyer.jui.data.manga +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.GetMangaLibraryQuery import ca.gosyer.jui.data.graphql.GetMangaQuery import ca.gosyer.jui.data.graphql.GetThumbnailUrlQuery @@ -26,10 +27,13 @@ import ca.gosyer.jui.data.graphql.type.MangaStatus as GqlMangaStatus import ca.gosyer.jui.data.graphql.type.UpdateStrategy as GqlUpdateStrategy class MangaRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : MangaRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getManga(mangaId: Long): Flow = apolloClient.query( GetMangaQuery(mangaId.toInt()), @@ -70,7 +74,7 @@ class MangaRepositoryImpl( .toFlow() .map { val data = it.dataAssertNoErrors - http.get(data.manga.thumbnailUrl!!).bodyAsChannel() + http.value.get(data.manga.thumbnailUrl!!).bodyAsChannel() } override fun updateMangaMeta( diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt index 7e403349..05257def 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.data.settings +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.AboutServerQuery import ca.gosyer.jui.data.graphql.AllSettingsQuery import ca.gosyer.jui.data.graphql.SetSettingsMutation @@ -43,8 +44,11 @@ import ca.gosyer.jui.domain.settings.model.WebUIFlavor as DomainWebUIFlavor import ca.gosyer.jui.domain.settings.model.WebUIInterface as DomainWebUIInterface class SettingsRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, ) : SettingsRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + private fun SettingsTypeFragment.toSettings() = Settings( authMode = authMode.toDomain(), diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/source/SourceRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/source/SourceRepositoryImpl.kt index 4fd2407b..5ea12525 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/source/SourceRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/source/SourceRepositoryImpl.kt @@ -1,5 +1,6 @@ package ca.gosyer.jui.data.source +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.graphql.FetchLatestMangaMutation import ca.gosyer.jui.data.graphql.FetchPopularMangaMutation import ca.gosyer.jui.data.graphql.FetchSearchMangaMutation @@ -33,10 +34,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class SourceRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : SourceRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getSourceList(): Flow> = apolloClient.query( GetSourceListQuery(), diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/updates/UpdatesRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/updates/UpdatesRepositoryImpl.kt index 68d75d6c..379e0dda 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/updates/UpdatesRepositoryImpl.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.data.updates +import ca.gosyer.jui.data.ApolloAppClient import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl.Companion.toMangaAndChapter import ca.gosyer.jui.data.graphql.GetChapterUpdatesQuery import ca.gosyer.jui.data.graphql.UpdateCategoryMutation @@ -19,10 +20,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class UpdatesRepositoryImpl( - private val apolloClient: ApolloClient, + private val apolloAppClient: ApolloAppClient, private val http: Http, private val serverUrl: Url, ) : UpdatesRepository { + val apolloClient: ApolloClient + get() = apolloAppClient.value + override fun getRecentUpdates(pageNum: Int): Flow = apolloClient.query( GetChapterUpdatesQuery(50, pageNum * 50), diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt index 90280ac0..372fded4 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt @@ -99,7 +99,7 @@ suspend fun main() { serverService.initialized .filter { it == ServerResult.STARTED || it == ServerResult.UNUSED } .onEach { - appComponent.downloadService.init() + appComponent.downloadService.getSubscription().launchIn(GlobalScope) appComponent.libraryUpdateService.init() } .launchIn(GlobalScope) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt index 311f0402..6db0056b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt @@ -118,11 +118,6 @@ interface SharedDomainComponent : CoreComponent { val libraryUpdateServiceFactory: LibraryUpdateService get() = LibraryUpdateService(serverPreferences, http) - @get:AppScope - @get:Provides - val downloadServiceFactory: DownloadService - get() = DownloadService(serverPreferences, http) - @get:AppScope @get:Provides val serverListenersFactory: ServerListeners diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/base/WebsocketService.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/base/WebsocketService.kt index 95ff3980..22d538cb 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/base/WebsocketService.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/base/WebsocketService.kt @@ -45,8 +45,8 @@ abstract class WebsocketService( fun init() { errorConnectionCount = 0 job?.cancel() - job = serverUrl - .mapLatest { serverUrl -> + job = client + .mapLatest { client -> status.value = Status.STARTING while (true) { if (errorConnectionCount > 3) { @@ -54,6 +54,7 @@ abstract class WebsocketService( throw CancellationException("Finish") } runCatching { + val serverUrl = serverUrl.value client.ws( host = serverUrl.host, port = serverUrl.port, diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadChapter.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadChapter.kt index 648a50e6..3b63a869 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadChapter.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadChapter.kt @@ -6,19 +6,24 @@ package ca.gosyer.jui.domain.download.model -import androidx.compose.runtime.Immutable -import ca.gosyer.jui.domain.chapter.model.Chapter -import ca.gosyer.jui.domain.manga.model.Manga -import kotlinx.serialization.Serializable - -@Serializable -@Immutable -data class DownloadChapter( - val chapterIndex: Int, - val mangaId: Long, - val chapter: Chapter, - val manga: Manga, - val state: DownloadState = DownloadState.Queued, - val progress: Float = 0f, - val tries: Int = 0, +data class DownloadQueueItem( + val position: Int, + val progress: Float, + val state: DownloadState, + val tries: Int, + val chapter: DownloadChapter, + val manga: DownloadManga +) + +data class DownloadChapter( + val id: Long, + val name: String, + val pageCount: Int, +) + +data class DownloadManga( + val id: Long, + val title: String, + val thumbnailUrl: String?, + val thumbnailUrlLastFetched: Long = 0, ) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadState.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadState.kt index f0e9eaf3..f85852fc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadState.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadState.kt @@ -6,16 +6,9 @@ package ca.gosyer.jui.domain.download.model -import androidx.compose.runtime.Stable -import kotlinx.serialization.Serializable - -@Serializable -@Stable -enum class DownloadState( - val state: Int, -) { - Queued(0), - Downloading(1), - Finished(2), - Error(3), +enum class DownloadState { + QUEUED, + DOWNLOADING, + FINISHED, + ERROR } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadStatus.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadStatus.kt index 1e98bc18..6ff71795 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadStatus.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadStatus.kt @@ -6,12 +6,7 @@ package ca.gosyer.jui.domain.download.model -import androidx.compose.runtime.Immutable -import kotlinx.serialization.Serializable - -@Serializable -@Immutable data class DownloadStatus( - val status: DownloaderStatus, - val queue: List, + val status: DownloaderState, + val queue: List, ) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadUpdate.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadUpdate.kt new file mode 100644 index 00000000..cf69583a --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadUpdate.kt @@ -0,0 +1,17 @@ +package ca.gosyer.jui.domain.download.model + +data class DownloadUpdate( + val type: DownloadUpdateType? = null, + val download: DownloadQueueItem? = null +) + +enum class DownloadUpdateType { + QUEUED, + DEQUEUED, + PAUSED, + STOPPED, + PROGRESS, + FINISHED, + ERROR, + POSITION +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadUpdates.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadUpdates.kt new file mode 100644 index 00000000..f70cfe8a --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloadUpdates.kt @@ -0,0 +1,8 @@ +package ca.gosyer.jui.domain.download.model + +data class DownloadUpdates( + val initial: List? = null, + val omittedUpdates: Boolean, + val state: DownloaderState, + val updates: List? = null +) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloaderStatus.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloaderStatus.kt index 785ba6cb..0dd480d5 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloaderStatus.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/model/DownloaderStatus.kt @@ -6,12 +6,7 @@ package ca.gosyer.jui.domain.download.model -import androidx.compose.runtime.Stable -import kotlinx.serialization.Serializable - -@Serializable -@Stable -enum class DownloaderStatus { - Started, - Stopped, +enum class DownloaderState { + STARTED, + STOPPED } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadRepository.kt index 7ea47b6c..e2d71d74 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadRepository.kt @@ -6,6 +6,8 @@ package ca.gosyer.jui.domain.download.service +import ca.gosyer.jui.domain.download.model.DownloadStatus +import ca.gosyer.jui.domain.download.model.DownloadUpdates import kotlinx.coroutines.flow.Flow interface DownloadRepository { @@ -25,4 +27,8 @@ interface DownloadRepository { ): Flow fun batchDownload(chapterIds: List): Flow + + fun downloadSubscription(): Flow + + fun downloadStatus(): Flow } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt index 06b07405..f61fd565 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt @@ -7,49 +7,144 @@ package ca.gosyer.jui.domain.download.service import ca.gosyer.jui.domain.base.WebsocketService -import ca.gosyer.jui.domain.download.model.DownloadChapter -import ca.gosyer.jui.domain.download.model.DownloadStatus -import ca.gosyer.jui.domain.download.model.DownloaderStatus -import ca.gosyer.jui.domain.server.Http -import ca.gosyer.jui.domain.server.service.ServerPreferences -import io.ktor.websocket.Frame -import io.ktor.websocket.readText +import ca.gosyer.jui.domain.download.model.DownloadQueueItem +import ca.gosyer.jui.domain.download.model.DownloadUpdateType +import ca.gosyer.jui.domain.download.model.DownloaderState +import com.diamondedge.logging.logging +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.update import me.tatarka.inject.annotations.Inject @Inject class DownloadService( - serverPreferences: ServerPreferences, - client: Http, -) : WebsocketService(serverPreferences, client) { - override val status: MutableStateFlow - get() = DownloadService.status + private val downloadRepository: DownloadRepository, +) { + private val log = logging() - override val query: String - get() = "/api/v1/downloads" + fun getSubscription(): Flow { + return downloadRepository.downloadSubscription() + .onStart { + log.info { "Starting download status subscription" } + status.value = WebsocketService.Status.STARTING + } + .catch { error -> + log.error(error) { "Error in download status subscription" } + status.value = WebsocketService.Status.STOPPED + } + .map { updates -> + status.value = WebsocketService.Status.RUNNING + if (updates.omittedUpdates) { + log.info { "Omitted updates detected, fetching fresh download status" } + fetchDownloadStatus() + return@map + } + if (updates.initial != null) { + downloadQueue.value = updates.initial + } + downloaderStatus.value = updates.state + updates.updates?.forEach { update -> + when (update.type) { + DownloadUpdateType.QUEUED -> { + update.download?.let { download -> + downloadQueue.update { + it.toMutableList().apply { + add(download.position.coerceAtMost(it.size), download) + } + } + } + } + DownloadUpdateType.DEQUEUED -> { + downloadQueue.update { + it.filter { it.chapter.id != update.download?.chapter?.id } + } + } + DownloadUpdateType.PAUSED -> { + downloaderStatus.value = DownloaderState.STOPPED + } + DownloadUpdateType.STOPPED -> { + downloaderStatus.value = DownloaderState.STOPPED + } + DownloadUpdateType.ERROR -> { + update.download?.let { download -> + downloadQueue.update { + it.map { chapter -> + if (chapter.chapter.id == download.chapter.id) { + chapter.copy(state = download.state) + } else { + chapter + } + } + } + } + } + DownloadUpdateType.PROGRESS -> { + update.download?.let { download -> + downloadQueue.update { + it.map { chapter -> + if (chapter.chapter.id == download.chapter.id) { + chapter.copy(progress = download.progress) + } else { + chapter + } + } + } + } + } + DownloadUpdateType.FINISHED -> { + downloadQueue.update { + it.filter { it.chapter.id != update.download?.chapter?.id } + } + } + DownloadUpdateType.POSITION -> { + update.download?.let { download -> + downloadQueue.update { + val index = it.indexOfFirst { it.chapter.id == download.chapter.id } + if (index != -1) { + it.toMutableList().apply { + removeAt(index) + add(download.position.coerceAtMost(it.size), download) + }.toList() + } else it + } - override suspend fun onReceived(frame: Frame.Text) { - val status = json.decodeFromString(frame.readText()) - downloaderStatus.value = status.status - downloadQueue.value = status.queue + } + } + null -> { + // todo Handle null case + } + } + } + } + } + + private suspend fun fetchDownloadStatus() { + val status = downloadRepository.downloadStatus().firstOrNull() + if (status != null) { + downloadQueue.value = status.queue + downloaderStatus.value = status.status + } } companion object { - val status = MutableStateFlow(Status.STARTING) - val downloadQueue = MutableStateFlow(emptyList()) - val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped) + val status = MutableStateFlow(WebsocketService.Status.STARTING) + val downloadQueue = MutableStateFlow(emptyList()) + val downloaderStatus = MutableStateFlow(DownloaderState.STOPPED) fun registerWatch(mangaId: Long) = downloadQueue .map { - it.filter { it.mangaId == mangaId } + it.filter { it.manga.id == mangaId } } fun registerWatches(mangaIds: Set) = downloadQueue .map { - it.filter { it.mangaId in mangaIds } + it.filter { it.manga.id in mangaIds } } } } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt index cab143d5..8cccf45b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt @@ -29,49 +29,64 @@ import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.websocket.WebSockets import io.ktor.http.URLBuilder import io.ktor.http.URLProtocol +import io.ktor.http.Url import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import io.ktor.client.plugins.auth.Auth as AuthPlugin -typealias Http = HttpClient +typealias Http = StateFlow expect val Engine: HttpClientEngineFactory expect fun HttpClientConfig.configurePlatform() -fun httpClient( - serverPreferences: ServerPreferences, - json: Json, -): Http = - HttpClient(Engine) { +private fun getHttpClient( + serverUrl: Url, + proxy: Proxy, + proxyHttpHost: String, + proxyHttpPort: Int, + proxySocksHost: String, + proxySocksPort: Int, + auth: Auth, + authUsername: String, + authPassword: String, + json: Json +): HttpClient { + return HttpClient(Engine) { configurePlatform() expectSuccess = true defaultRequest { - url(serverPreferences.serverUrl().get().toString()) + url(serverUrl.toString()) } engine { - proxy = when (serverPreferences.proxy().get()) { + this.proxy = when (proxy) { Proxy.NO_PROXY -> null Proxy.HTTP_PROXY -> ProxyBuilder.http( URLBuilder( - host = serverPreferences.proxyHttpHost().get(), - port = serverPreferences.proxyHttpPort().get(), + host = proxyHttpHost, + port = proxyHttpPort, ).build(), ) Proxy.SOCKS_PROXY -> ProxyBuilder.socks( - serverPreferences.proxySocksHost().get(), - serverPreferences.proxySocksPort().get(), + proxySocksHost, + proxySocksPort, ) } } - when (serverPreferences.auth().get()) { + when (auth) { Auth.NONE -> Unit Auth.BASIC -> AuthPlugin { @@ -81,8 +96,8 @@ fun httpClient( } credentials { BasicAuthCredentials( - serverPreferences.authUsername().get(), - serverPreferences.authPassword().get(), + authUsername, + authPassword, ) } } @@ -92,8 +107,8 @@ fun httpClient( digest { credentials { DigestAuthCredentials( - serverPreferences.authUsername().get(), - serverPreferences.authPassword().get(), + authUsername, + authPassword, ) } } @@ -123,3 +138,48 @@ fun httpClient( } } } +} + +@OptIn(DelicateCoroutinesApi::class) +fun httpClient( + serverPreferences: ServerPreferences, + json: Json, +): Http = combine( + serverPreferences.serverUrl().stateIn(GlobalScope), + serverPreferences.proxy().stateIn(GlobalScope), + serverPreferences.proxyHttpHost().stateIn(GlobalScope), + serverPreferences.proxyHttpPort().stateIn(GlobalScope), + serverPreferences.proxySocksHost().stateIn(GlobalScope), + serverPreferences.proxySocksPort().stateIn(GlobalScope), + serverPreferences.auth().stateIn(GlobalScope), + serverPreferences.authUsername().stateIn(GlobalScope), + serverPreferences.authPassword().stateIn(GlobalScope), +) { + getHttpClient( + it[0] as Url, + it[1] as Proxy, + it[2] as String, + it[3] as Int, + it[4] as String, + it[5] as Int, + it[6] as Auth, + it[7] as String, + it[8] as String, + json, + ) +}.stateIn( + GlobalScope, + SharingStarted.Eagerly, + getHttpClient( + serverPreferences.serverUrl().get(), + serverPreferences.proxy().get(), + serverPreferences.proxyHttpHost().get(), + serverPreferences.proxyHttpPort().get(), + serverPreferences.proxySocksHost().get(), + serverPreferences.proxySocksPort().get(), + serverPreferences.auth().get(), + serverPreferences.authUsername().get(), + serverPreferences.authPassword().get(), + json, + ) +) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt index 9279b9c0..1ef4afd3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt @@ -39,7 +39,7 @@ class UpdateChecker( fun asFlow(manualFetch: Boolean) = flow { if (!manualFetch && !updatePreferences.enabled().get()) return@flow - val latestRelease = client.get( + val latestRelease = client.value.get( "https://api.github.com/repos/$GITHUB_REPO/releases/latest", ).body() diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt index ea120f26..d234565a 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt @@ -33,7 +33,7 @@ actual fun ComponentRegistryBuilder.register( contextWrapper: ContextWrapper, http: Http, ) { - setupDefaultComponents(contextWrapper, httpClient = { http }) + setupDefaultComponents(contextWrapper, httpClient = { http.value }) } actual fun DiskCacheBuilder.configure( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt index 4b1661f9..07d3603f 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.unit.dp import ca.gosyer.jui.domain.chapter.interactor.DeleteChapterDownload import ca.gosyer.jui.domain.chapter.model.Chapter import ca.gosyer.jui.domain.download.interactor.StopChapterDownload -import ca.gosyer.jui.domain.download.model.DownloadChapter +import ca.gosyer.jui.domain.download.model.DownloadQueueItem import ca.gosyer.jui.domain.download.model.DownloadState import ca.gosyer.jui.domain.manga.model.Manga import ca.gosyer.jui.i18n.MR @@ -61,12 +61,12 @@ data class ChapterDownloadItem( ) val downloadState = _downloadState.asStateFlow() - private val _downloadChapterFlow: MutableStateFlow = MutableStateFlow(null) + private val _downloadChapterFlow: MutableStateFlow = MutableStateFlow(null) val downloadChapterFlow = _downloadChapterFlow.asStateFlow() - fun updateFrom(downloadingChapters: List) { + fun updateFrom(downloadingChapters: List) { val downloadingChapter = downloadingChapters.find { - it.chapterIndex == chapter.index && it.mangaId == chapter.mangaId + it.chapter.id == chapter.id } if (downloadingChapter != null && downloadState.value != ChapterDownloadState.Downloading) { _downloadState.value = ChapterDownloadState.Downloading @@ -155,11 +155,11 @@ private fun DownloadIconButton(onClick: () -> Unit) { @Composable private fun DownloadingIconButton( - downloadChapter: DownloadChapter?, + downloadChapter: DownloadQueueItem?, onClick: () -> Unit, ) { DropdownIconButton( - downloadChapter?.mangaId to downloadChapter?.chapterIndex, + downloadChapter?.chapter?.id, { DropdownMenuItem(onClick = onClick) { Text(stringResource(MR.strings.action_cancel)) @@ -167,7 +167,7 @@ private fun DownloadingIconButton( }, ) { when (downloadChapter?.state) { - null, DownloadState.Queued -> CircularProgressIndicator( + null, DownloadState.QUEUED -> CircularProgressIndicator( Modifier .size(26.dp) .padding(2.dp), @@ -175,7 +175,7 @@ private fun DownloadingIconButton( 2.dp, ) - DownloadState.Downloading -> if (downloadChapter.progress != 0.0F) { + DownloadState.DOWNLOADING -> if (downloadChapter.progress != 0.0F) { val animatedProgress by animateFloatAsState( targetValue = downloadChapter.progress, animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, @@ -206,7 +206,7 @@ private fun DownloadingIconButton( ) } - DownloadState.Error -> Surface(shape = CircleShape, color = LocalContentColor.current) { + DownloadState.ERROR -> Surface(shape = CircleShape, color = LocalContentColor.current) { Icon( Icons.Rounded.Error, null, @@ -217,7 +217,7 @@ private fun DownloadingIconButton( ) } - DownloadState.Finished -> Surface(shape = CircleShape, color = LocalContentColor.current) { + DownloadState.FINISHED -> Surface(shape = CircleShape, color = LocalContentColor.current) { Icon( Icons.Rounded.Check, null, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt index d5a60040..97cd01e3 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt @@ -6,6 +6,7 @@ package ca.gosyer.jui.ui.base.image +import ca.gosyer.jui.domain.download.model.DownloadManga import ca.gosyer.jui.domain.extension.model.Extension import ca.gosyer.jui.domain.manga.model.Manga import ca.gosyer.jui.domain.server.Http @@ -42,10 +43,13 @@ class ImageLoaderProvider( fun get(imageCache: ImageCache): ImageLoader = ImageLoader { components { + add(KtorUrlFetcher.Factory { http.value }) register(context, http) add(MokoResourceFetcher.Factory()) add(MangaCoverMapper()) add(MangaCoverKeyer()) + add(DownloadMangaCoverMapper()) + add(DownloadMangaCoverKeyer()) add(ExtensionIconMapper()) add(ExtensionIconKeyer()) add(SourceIconMapper()) @@ -77,7 +81,28 @@ class ImageLoaderProvider( options: Options, ): String? { if (data !is Manga) return null - return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" + return "${data.id}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" + } + } + + inner class DownloadMangaCoverMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is DownloadManga) return null + if (data.thumbnailUrl.isNullOrBlank()) return null + return Url(serverUrl.value.toString() + data.thumbnailUrl) + } + } + + class DownloadMangaCoverKeyer : Keyer { + override fun key( + data: Any, + options: Options, + ): String? { + if (data !is DownloadManga) return null + return "${data.id}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/KtorUrlFetcher.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/KtorUrlFetcher.kt new file mode 100644 index 00000000..a4cb0480 --- /dev/null +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/KtorUrlFetcher.kt @@ -0,0 +1,84 @@ +package ca.gosyer.jui.ui.base.image + +import com.seiko.imageloader.component.fetcher.FetchResult +import com.seiko.imageloader.component.fetcher.Fetcher +import com.seiko.imageloader.model.ImageSource +import com.seiko.imageloader.model.ImageSourceFrom +import com.seiko.imageloader.model.KtorRequestData +import com.seiko.imageloader.model.extraData +import com.seiko.imageloader.model.mimeType +import com.seiko.imageloader.model.toImageSource +import com.seiko.imageloader.option.Options +import io.ktor.client.HttpClient +import io.ktor.client.request.headers +import io.ktor.client.request.prepareRequest +import io.ktor.client.request.url +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.HttpMethod +import io.ktor.http.Url +import io.ktor.http.contentType +import io.ktor.http.isSuccess +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.readRemaining +import kotlinx.io.readByteArray +import okio.Buffer + +class KtorUrlFetcher private constructor( + private val httpUrl: Url, + private val httpClient: () -> HttpClient, + private val ktorRequestData: KtorRequestData?, +) : Fetcher { + + override suspend fun fetch(): FetchResult { + return httpClient().prepareRequest { + url(httpUrl) + method = ktorRequestData?.method ?: HttpMethod.Get + ktorRequestData?.headers?.let { + headers { + it.forEach { (key, value) -> + append(key, value) + } + } + } + }.execute { response -> + if (!response.status.isSuccess()) { + throw KtorUrlRequestException("code:${response.status.value}, ${response.status.description}") + } + + FetchResult.OfSource( + imageSource = channelToImageSource(response.bodyAsChannel()), + imageSourceFrom = ImageSourceFrom.Network, + extra = extraData { + mimeType(response.contentType()?.toString()) + }, + ) + } + } + + class Factory( + private val httpClient: () -> HttpClient, + ) : Fetcher.Factory { + override fun create(data: Any, options: Options): Fetcher? { + if (data !is Url) return null + return KtorUrlFetcher( + httpUrl = data, + httpClient = httpClient, + ktorRequestData = options.extra["KEY_KTOR_REQUEST_DATA"] as? KtorRequestData, + ) + } + } +} + +private suspend fun channelToImageSource(channel: ByteReadChannel): ImageSource { + val buffer = Buffer() + while (!channel.isClosedForRead) { + val packet = channel.readRemaining(2048) + while (!packet.exhausted()) { + val bytes = packet.readByteArray() + buffer.write(bytes) + } + } + return buffer.toImageSource() +} + +private class KtorUrlRequestException(msg: String) : RuntimeException(msg) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt index 9e916f0e..073f551a 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt @@ -7,13 +7,13 @@ package ca.gosyer.jui.ui.downloads import ca.gosyer.jui.domain.base.WebsocketService.Actions -import ca.gosyer.jui.domain.chapter.model.Chapter import ca.gosyer.jui.domain.download.interactor.ClearDownloadQueue import ca.gosyer.jui.domain.download.interactor.QueueChapterDownload import ca.gosyer.jui.domain.download.interactor.ReorderChapterDownload import ca.gosyer.jui.domain.download.interactor.StartDownloading import ca.gosyer.jui.domain.download.interactor.StopChapterDownload import ca.gosyer.jui.domain.download.interactor.StopDownloading +import ca.gosyer.jui.domain.download.model.DownloadChapter import ca.gosyer.jui.domain.download.service.DownloadService import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ViewModel @@ -69,35 +69,35 @@ class DownloadsScreenViewModel( scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) } } - fun stopDownload(chapter: Chapter) { + fun stopDownload(chapter: DownloadChapter) { scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) } } - fun moveUp(chapter: Chapter) { + fun moveUp(chapter: DownloadChapter) { scope.launch { val index = - downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } + downloadQueue.value.indexOfFirst { it.chapter.id == chapter.id } if (index == -1 || index <= 0) return@launch reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) }) } } - fun moveDown(chapter: Chapter) { + fun moveDown(chapter: DownloadChapter) { scope.launch { val index = - downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } + downloadQueue.value.indexOfFirst { it.chapter.id == chapter.id } if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) }) } } - fun moveToTop(chapter: Chapter) { + fun moveToTop(chapter: DownloadChapter) { scope.launch { reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) }) } } - fun moveToBottom(chapter: Chapter) { + fun moveToBottom(chapter: DownloadChapter) { scope.launch { reorderChapterDownload.await( chapter.id, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt index 789efc31..e4b52fed 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/components/DownloadsScreenContent.kt @@ -48,9 +48,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import ca.gosyer.jui.domain.chapter.model.Chapter import ca.gosyer.jui.domain.download.model.DownloadChapter -import ca.gosyer.jui.domain.download.model.DownloaderStatus +import ca.gosyer.jui.domain.download.model.DownloadQueueItem +import ca.gosyer.jui.domain.download.model.DownloaderState import ca.gosyer.jui.i18n.MR import ca.gosyer.jui.ui.base.navigation.ActionItem import ca.gosyer.jui.ui.base.navigation.Toolbar @@ -71,17 +71,17 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun DownloadsScreenContent( - downloadQueue: ImmutableList, - downloadStatus: DownloaderStatus, + downloadQueue: ImmutableList, + downloadStatus: DownloaderState, startDownloading: () -> Unit, pauseDownloading: () -> Unit, clearQueue: () -> Unit, onMangaClick: (Long) -> Unit, - stopDownload: (Chapter) -> Unit, - moveDownloadUp: (Chapter) -> Unit, - moveDownloadDown: (Chapter) -> Unit, - moveDownloadToTop: (Chapter) -> Unit, - moveDownloadToBottom: (Chapter) -> Unit, + stopDownload: (DownloadChapter) -> Unit, + moveDownloadUp: (DownloadChapter) -> Unit, + moveDownloadDown: (DownloadChapter) -> Unit, + moveDownloadToTop: (DownloadChapter) -> Unit, + moveDownloadToBottom: (DownloadChapter) -> Unit, ) { Scaffold( modifier = Modifier.windowInsetsPadding( @@ -114,11 +114,11 @@ fun DownloadsScreenContent( ), ).asPaddingValues(), ) { - items(downloadQueue, key = { "${it.mangaId}-${it.chapterIndex}" }) { + items(downloadQueue, key = { it.chapter.id }) { DownloadsItem( modifier = Modifier.animateItem(), item = it, - onClickCover = { onMangaClick(it.mangaId) }, + onClickCover = { onMangaClick(it.manga.id) }, onClickCancel = stopDownload, onClickMoveUp = moveDownloadUp, onClickMoveDown = moveDownloadDown, @@ -147,13 +147,13 @@ fun DownloadsScreenContent( @Composable fun DownloadsItem( modifier: Modifier = Modifier, - item: DownloadChapter, + item: DownloadQueueItem, onClickCover: () -> Unit, - onClickCancel: (Chapter) -> Unit, - onClickMoveUp: (Chapter) -> Unit, - onClickMoveDown: (Chapter) -> Unit, - onClickMoveToTop: (Chapter) -> Unit, - onClickMoveToBottom: (Chapter) -> Unit, + onClickCancel: (DownloadChapter) -> Unit, + onClickMoveUp: (DownloadChapter) -> Unit, + onClickMoveDown: (DownloadChapter) -> Unit, + onClickMoveToTop: (DownloadChapter) -> Unit, + onClickMoveToBottom: (DownloadChapter) -> Unit, ) { MangaListItem( modifier = modifier @@ -180,8 +180,8 @@ fun DownloadsItem( text = item.manga.title, fontWeight = FontWeight.SemiBold, ) - val progress = if (item.chapter.pageCount != null && item.chapter.pageCount != -1) { - " - " + "${(item.chapter.pageCount!! * item.progress).toInt()}/${item.chapter.pageCount}" + val progress = if (item.chapter.pageCount > 0) { + " - " + "${(item.chapter.pageCount * item.progress).toInt()}/${item.chapter.pageCount}" } else { "" } @@ -200,7 +200,7 @@ fun DownloadsItem( ) } DropdownIconButton( - item.mangaId to item.chapterIndex, + item.chapter.id, { DropdownMenuItem(onClick = { onClickCancel(item.chapter) }) { Text(stringResource(MR.strings.action_cancel)) @@ -231,13 +231,13 @@ fun DownloadsItem( @Stable @Composable private fun getActionItems( - downloadStatus: DownloaderStatus, + downloadStatus: DownloaderState, startDownloading: () -> Unit, pauseDownloading: () -> Unit, clearQueue: () -> Unit, ): ImmutableList = listOf( - if (downloadStatus == DownloaderStatus.Started) { + if (downloadStatus == DownloaderState.STARTED) { ActionItem( stringResource(MR.strings.action_pause), Icons.Rounded.Pause, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/DownloadsExtraInfo.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/DownloadsExtraInfo.kt index 76f68c18..6233475d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/DownloadsExtraInfo.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/DownloadsExtraInfo.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import ca.gosyer.jui.domain.base.WebsocketService -import ca.gosyer.jui.domain.download.model.DownloaderStatus +import ca.gosyer.jui.domain.download.model.DownloaderState import ca.gosyer.jui.i18n.MR import ca.gosyer.jui.ui.base.LocalViewModels import ca.gosyer.jui.uicore.resources.stringResource @@ -43,7 +43,7 @@ fun DownloadsExtraInfo() { WebsocketService.Status.RUNNING -> { if (list.isNotEmpty()) { val remainingDownloads = stringResource(MR.strings.downloads_remaining, list.size) - if (downloaderStatus == DownloaderStatus.Stopped) { + if (downloaderStatus == DownloaderState.STOPPED) { stringResource(MR.strings.downloads_paused) + " • " + remainingDownloads } else { remainingDownloads diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt index 08dd8d81..efc57006 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt @@ -96,7 +96,7 @@ class UpdatesScreenViewModel( } .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) .onEach { (mangaIds, queue) -> - val chapters = queue.filter { it.mangaId in mangaIds } + val chapters = queue.filter { it.manga.id in mangaIds } updates.value.filterIsInstance().forEach { it.chapterDownloadItem.updateFrom(chapters) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt index e1e37913..435f6be1 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt @@ -24,7 +24,7 @@ actual fun ComponentRegistryBuilder.register( contextWrapper: ContextWrapper, http: Http, ) { - setupDefaultComponents(httpClient = { http }) + setupDefaultComponents(httpClient = { http.value }) } actual fun DiskCacheBuilder.configure( diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/downloads/DesktopDownloadService.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/downloads/DesktopDownloadService.kt index 0314c1a0..15f85ed9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/downloads/DesktopDownloadService.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/downloads/DesktopDownloadService.kt @@ -9,11 +9,13 @@ package ca.gosyer.jui.ui.downloads import ca.gosyer.jui.domain.base.WebsocketService import ca.gosyer.jui.domain.download.service.DownloadService import ca.gosyer.jui.uicore.vm.ContextWrapper +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.launchIn internal actual fun startDownloadService( contextWrapper: ContextWrapper, downloadService: DownloadService, actions: WebsocketService.Actions, ) { - downloadService.init() + downloadService.getSubscription().launchIn(GlobalScope) } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt index 4cd1069e..03942edd 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt @@ -28,7 +28,7 @@ suspend fun imageFromUrl( url: String, block: HttpRequestBuilder.() -> Unit, ): ImageBitmap = - client.get(url) { + client.value.get(url) { expectSuccess = true block() }.toImageBitmap() diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt index d0732c19..059feb86 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt @@ -29,7 +29,7 @@ actual fun ComponentRegistryBuilder.register( contextWrapper: ContextWrapper, http: Http, ) { - setupDefaultComponents(httpClient = { http }) + setupDefaultComponents(httpClient = { http.value }) } actual fun DiskCacheBuilder.configure( diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/downloads/IosDownloadService.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/downloads/IosDownloadService.kt index 0314c1a0..15f85ed9 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/downloads/IosDownloadService.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/downloads/IosDownloadService.kt @@ -9,11 +9,13 @@ package ca.gosyer.jui.ui.downloads import ca.gosyer.jui.domain.base.WebsocketService import ca.gosyer.jui.domain.download.service.DownloadService import ca.gosyer.jui.uicore.vm.ContextWrapper +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.launchIn internal actual fun startDownloadService( contextWrapper: ContextWrapper, downloadService: DownloadService, actions: WebsocketService.Actions, ) { - downloadService.init() + downloadService.getSubscription().launchIn(GlobalScope) }