mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Implement Downloads GQL and fix url changes requiring restarts.
This commit is contained in:
@@ -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<Frame.Text>()
|
||||
.map { json.decodeFromString<DownloadStatus>(it.readText()) }
|
||||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.mapLatest(::onReceived)
|
||||
.catch {
|
||||
log.warn(it) { "Error running downloader" }
|
||||
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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
fragment DownloadFragment on DownloadType {
|
||||
chapter {
|
||||
id
|
||||
name
|
||||
pageCount
|
||||
}
|
||||
manga {
|
||||
id
|
||||
title
|
||||
thumbnailUrl
|
||||
thumbnailUrlLastFetched
|
||||
}
|
||||
position
|
||||
progress
|
||||
state
|
||||
tries
|
||||
}
|
||||
@@ -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())
|
||||
typealias ApolloAppClient = StateFlow<ApolloClient>
|
||||
|
||||
private fun getApolloClient(
|
||||
httpClient: HttpClient,
|
||||
serverUrl: Url,
|
||||
): ApolloClient {
|
||||
val url = URLBuilder(serverUrl)
|
||||
.appendPathSegments("api", "graphql")
|
||||
.buildString(),
|
||||
)
|
||||
.ktorClient(http)
|
||||
.wsProtocol(GraphQLWsProtocol.Factory())
|
||||
.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())
|
||||
}
|
||||
|
||||
@@ -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<BackupValidationResult> =
|
||||
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"]!!
|
||||
|
||||
@@ -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<List<Category>> =
|
||||
apolloClient.query(
|
||||
GetMangaCategoriesQuery(mangaId.toInt()),
|
||||
|
||||
@@ -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<Chapter> =
|
||||
apolloClient.query(
|
||||
GetChapterQuery(chapterId.toInt()),
|
||||
@@ -164,7 +168,7 @@ class ChapterRepositoryImpl(
|
||||
): Flow<ByteArray> {
|
||||
val realUrl = Url("$serverUrl$url")
|
||||
|
||||
return flow { emit(http.get(realUrl, block).readRawBytes()) }
|
||||
return flow { emit(http.value.get(realUrl, block).readRawBytes()) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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<Unit> =
|
||||
apolloClient.mutation(
|
||||
StartDownloaderMutation(),
|
||||
@@ -90,4 +109,82 @@ class DownloadRepositoryImpl(
|
||||
.map {
|
||||
it.dataAssertNoErrors
|
||||
}
|
||||
|
||||
override fun downloadSubscription(): Flow<DownloadUpdates> =
|
||||
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<DownloadStatus> =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Extension>> =
|
||||
apolloClient.mutation(
|
||||
FetchExtensionsMutation(),
|
||||
|
||||
@@ -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<GlobalMeta> =
|
||||
apolloClient.query(
|
||||
GetGlobalMetaQuery(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Manga> =
|
||||
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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<List<Source>> =
|
||||
apolloClient.query(
|
||||
GetSourceListQuery(),
|
||||
|
||||
@@ -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<Updates> =
|
||||
apolloClient.query(
|
||||
GetChapterUpdatesQuery(50, pageNum * 50),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<DownloadChapter>,
|
||||
val status: DownloaderState,
|
||||
val queue: List<DownloadQueueItem>,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ca.gosyer.jui.domain.download.model
|
||||
|
||||
data class DownloadUpdates(
|
||||
val initial: List<DownloadQueueItem>? = null,
|
||||
val omittedUpdates: Boolean,
|
||||
val state: DownloaderState,
|
||||
val updates: List<DownloadUpdate>? = null
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<Unit>
|
||||
|
||||
fun batchDownload(chapterIds: List<Long>): Flow<Unit>
|
||||
|
||||
fun downloadSubscription(): Flow<DownloadUpdates>
|
||||
|
||||
fun downloadStatus(): Flow<DownloadStatus>
|
||||
}
|
||||
|
||||
@@ -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<Status>
|
||||
get() = DownloadService.status
|
||||
private val downloadRepository: DownloadRepository,
|
||||
) {
|
||||
private val log = logging()
|
||||
|
||||
override val query: String
|
||||
get() = "/api/v1/downloads"
|
||||
fun getSubscription(): Flow<Unit> {
|
||||
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<DownloadStatus>(frame.readText())
|
||||
downloaderStatus.value = status.status
|
||||
}
|
||||
}
|
||||
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<DownloadChapter>())
|
||||
val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
|
||||
val status = MutableStateFlow(WebsocketService.Status.STARTING)
|
||||
val downloadQueue = MutableStateFlow(emptyList<DownloadQueueItem>())
|
||||
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<Long>) =
|
||||
downloadQueue
|
||||
.map {
|
||||
it.filter { it.mangaId in mangaIds }
|
||||
it.filter { it.manga.id in mangaIds }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HttpClient>
|
||||
|
||||
expect val Engine: HttpClientEngineFactory<HttpClientEngineConfig>
|
||||
|
||||
expect fun HttpClientConfig<HttpClientEngineConfig>.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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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<GithubRelease>()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<DownloadChapter?> = MutableStateFlow(null)
|
||||
private val _downloadChapterFlow: MutableStateFlow<DownloadQueueItem?> = MutableStateFlow(null)
|
||||
val downloadChapterFlow = _downloadChapterFlow.asStateFlow()
|
||||
|
||||
fun updateFrom(downloadingChapters: List<DownloadChapter>) {
|
||||
fun updateFrom(downloadingChapters: List<DownloadQueueItem>) {
|
||||
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,
|
||||
|
||||
@@ -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<Url> {
|
||||
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}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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<DownloadChapter>,
|
||||
downloadStatus: DownloaderStatus,
|
||||
downloadQueue: ImmutableList<DownloadQueueItem>,
|
||||
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<ActionItem> =
|
||||
listOf(
|
||||
if (downloadStatus == DownloaderStatus.Started) {
|
||||
if (downloadStatus == DownloaderState.STARTED) {
|
||||
ActionItem(
|
||||
stringResource(MR.strings.action_pause),
|
||||
Icons.Rounded.Pause,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<UpdatesUI.Item>().forEach {
|
||||
it.chapterDownloadItem.updateFrom(chapters)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ actual fun ComponentRegistryBuilder.register(
|
||||
contextWrapper: ContextWrapper,
|
||||
http: Http,
|
||||
) {
|
||||
setupDefaultComponents(httpClient = { http })
|
||||
setupDefaultComponents(httpClient = { http.value })
|
||||
}
|
||||
|
||||
actual fun DiskCacheBuilder.configure(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ suspend fun imageFromUrl(
|
||||
url: String,
|
||||
block: HttpRequestBuilder.() -> Unit,
|
||||
): ImageBitmap =
|
||||
client.get(url) {
|
||||
client.value.get(url) {
|
||||
expectSuccess = true
|
||||
block()
|
||||
}.toImageBitmap()
|
||||
|
||||
@@ -29,7 +29,7 @@ actual fun ComponentRegistryBuilder.register(
|
||||
contextWrapper: ContextWrapper,
|
||||
http: Http,
|
||||
) {
|
||||
setupDefaultComponents(httpClient = { http })
|
||||
setupDefaultComponents(httpClient = { http.value })
|
||||
}
|
||||
|
||||
actual fun DiskCacheBuilder.configure(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user