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.Actions
|
||||||
import ca.gosyer.jui.domain.base.WebsocketService.Status
|
import ca.gosyer.jui.domain.base.WebsocketService.Status
|
||||||
import ca.gosyer.jui.domain.download.model.DownloadState
|
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
|
||||||
import ca.gosyer.jui.domain.download.service.DownloadService.Companion.status
|
import ca.gosyer.jui.domain.download.service.DownloadService.Companion.status
|
||||||
import ca.gosyer.jui.i18n.MR
|
import ca.gosyer.jui.i18n.MR
|
||||||
import com.diamondedge.logging.logging
|
import com.diamondedge.logging.logging
|
||||||
import dev.icerock.moko.resources.desc.desc
|
import dev.icerock.moko.resources.desc.desc
|
||||||
import dev.icerock.moko.resources.format
|
import dev.icerock.moko.resources.format
|
||||||
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.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -42,13 +37,9 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collect
|
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.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@@ -154,31 +145,12 @@ class AndroidDownloadService : Service() {
|
|||||||
throw CancellationException()
|
throw CancellationException()
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
client.ws(
|
appComponent.downloadService
|
||||||
host = serverUrl.host,
|
.getSubscription()
|
||||||
port = serverUrl.port,
|
.onEach {
|
||||||
path = serverUrl.encodedPath + "/api/v1/downloads",
|
onReceived()
|
||||||
request = {
|
}
|
||||||
if (serverUrl.protocol == URLProtocol.HTTPS) {
|
.collect()
|
||||||
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" }
|
|
||||||
}
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}.throwIfCancellation().isFailure.let {
|
}.throwIfCancellation().isFailure.let {
|
||||||
status.value = Status.STARTING
|
status.value = Status.STARTING
|
||||||
if (it) errorConnectionCount++
|
if (it) errorConnectionCount++
|
||||||
@@ -193,13 +165,11 @@ class AndroidDownloadService : Service() {
|
|||||||
.launchIn(ioScope)
|
.launchIn(ioScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onReceived(status: DownloadStatus) {
|
private fun onReceived() {
|
||||||
DownloadService.downloaderStatus.value = status.status
|
val downloadingChapter = DownloadService.downloadQueue.value.lastOrNull { it.state == DownloadState.DOWNLOADING }
|
||||||
DownloadService.downloadQueue.value = status.queue
|
|
||||||
val downloadingChapter = status.queue.lastOrNull { it.state == DownloadState.Downloading }
|
|
||||||
if (downloadingChapter != null) {
|
if (downloadingChapter != null) {
|
||||||
val notification = with(progressNotificationBuilder) {
|
val notification = with(progressNotificationBuilder) {
|
||||||
val max = downloadingChapter.chapter.pageCount ?: 0
|
val max = downloadingChapter.chapter.pageCount
|
||||||
val current = (max * downloadingChapter.progress).toInt().coerceIn(0, max)
|
val current = (max * downloadingChapter.progress).toInt().coerceIn(0, max)
|
||||||
setProgress(max, current, false)
|
setProgress(max, current, false)
|
||||||
|
|
||||||
|
|||||||
@@ -39,3 +39,29 @@ mutation EnqueueChapterDownloads($ids: [Int!]!) {
|
|||||||
clientMutationId
|
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
|
package ca.gosyer.jui.data
|
||||||
|
|
||||||
|
import ca.gosyer.jui.core.di.AppScope
|
||||||
import ca.gosyer.jui.data.backup.BackupRepositoryImpl
|
import ca.gosyer.jui.data.backup.BackupRepositoryImpl
|
||||||
import ca.gosyer.jui.data.category.CategoryRepositoryImpl
|
import ca.gosyer.jui.data.category.CategoryRepositoryImpl
|
||||||
import ca.gosyer.jui.data.chapter.ChapterRepositoryImpl
|
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.source.service.SourceRepository
|
||||||
import ca.gosyer.jui.domain.updates.service.UpdatesRepository
|
import ca.gosyer.jui.domain.updates.service.UpdatesRepository
|
||||||
import com.apollographql.apollo.ApolloClient
|
import com.apollographql.apollo.ApolloClient
|
||||||
|
import com.apollographql.apollo.annotations.ApolloExperimental
|
||||||
import com.apollographql.apollo.network.ws.GraphQLWsProtocol
|
import com.apollographql.apollo.network.ws.GraphQLWsProtocol
|
||||||
import com.apollographql.ktor.ktorClient
|
import com.apollographql.ktor.ktorClient
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.http.URLBuilder
|
import io.ktor.http.URLBuilder
|
||||||
|
import io.ktor.http.Url
|
||||||
import io.ktor.http.appendPathSegments
|
import io.ktor.http.appendPathSegments
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.IO
|
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
|
import me.tatarka.inject.annotations.Provides
|
||||||
|
|
||||||
interface DataComponent : SharedDataComponent {
|
typealias ApolloAppClient = StateFlow<ApolloClient>
|
||||||
@Provides
|
|
||||||
fun apolloClient(
|
private fun getApolloClient(
|
||||||
http: Http,
|
httpClient: HttpClient,
|
||||||
serverPreferences: ServerPreferences,
|
serverUrl: Url,
|
||||||
) = ApolloClient.Builder()
|
): ApolloClient {
|
||||||
.serverUrl(
|
val url = URLBuilder(serverUrl)
|
||||||
URLBuilder(serverPreferences.serverUrl().get())
|
.appendPathSegments("api", "graphql")
|
||||||
.appendPathSegments("api", "graphql")
|
.buildString()
|
||||||
.buildString(),
|
return ApolloClient.Builder()
|
||||||
)
|
.serverUrl(url)
|
||||||
.ktorClient(http)
|
.ktorClient(httpClient)
|
||||||
.wsProtocol(GraphQLWsProtocol.Factory())
|
.wsProtocol(GraphQLWsProtocol.Factory(pingIntervalMillis = 30))
|
||||||
.dispatcher(Dispatchers.IO)
|
.dispatcher(Dispatchers.IO)
|
||||||
.build()
|
.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
|
@Provides
|
||||||
fun settingsRepository(apolloClient: ApolloClient): SettingsRepository = SettingsRepositoryImpl(apolloClient)
|
fun settingsRepository(apolloAppClient: ApolloAppClient): SettingsRepository = SettingsRepositoryImpl(apolloAppClient)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun categoryRepository(
|
fun categoryRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): CategoryRepository = CategoryRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): CategoryRepository = CategoryRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun chapterRepository(
|
fun chapterRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): ChapterRepository = ChapterRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): ChapterRepository = ChapterRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun downloadRepository(
|
fun downloadRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): DownloadRepository = DownloadRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): DownloadRepository = DownloadRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun extensionRepository(
|
fun extensionRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): ExtensionRepository = ExtensionRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): ExtensionRepository = ExtensionRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun globalRepository(
|
fun globalRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): GlobalRepository = GlobalRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): GlobalRepository = GlobalRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun libraryRepository(
|
fun libraryRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): LibraryRepository = LibraryRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): LibraryRepository = LibraryRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun mangaRepository(
|
fun mangaRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): MangaRepository = MangaRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): MangaRepository = MangaRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun sourceRepository(
|
fun sourceRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): SourceRepository = SourceRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): SourceRepository = SourceRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun updatesRepository(
|
fun updatesRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
serverPreferences: ServerPreferences,
|
||||||
): UpdatesRepository = UpdatesRepositoryImpl(apolloClient, http, serverPreferences.serverUrl().get())
|
): UpdatesRepository = UpdatesRepositoryImpl(apolloAppClient, http, serverPreferences.serverUrl().get())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun backupRepository(
|
fun backupRepository(
|
||||||
apolloClient: ApolloClient,
|
apolloAppClient: ApolloAppClient,
|
||||||
http: Http,
|
http: Http,
|
||||||
serverPreferences: ServerPreferences,
|
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
|
package ca.gosyer.jui.data.backup
|
||||||
|
|
||||||
import ca.gosyer.jui.core.io.toSource
|
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.CreateBackupMutation
|
||||||
import ca.gosyer.jui.data.graphql.RestoreBackupMutation
|
import ca.gosyer.jui.data.graphql.RestoreBackupMutation
|
||||||
import ca.gosyer.jui.data.graphql.RestoreStatusQuery
|
import ca.gosyer.jui.data.graphql.RestoreStatusQuery
|
||||||
@@ -24,10 +25,13 @@ import okio.Source
|
|||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
|
||||||
class BackupRepositoryImpl(
|
class BackupRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : BackupRepository {
|
) : BackupRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun validateBackup(source: Source): Flow<BackupValidationResult> =
|
override fun validateBackup(source: Source): Flow<BackupValidationResult> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
ValidateBackupQuery(
|
ValidateBackupQuery(
|
||||||
@@ -89,7 +93,7 @@ class BackupRepositoryImpl(
|
|||||||
.toFlow()
|
.toFlow()
|
||||||
.map {
|
.map {
|
||||||
val url = it.dataAssertNoErrors.createBackup.url
|
val url = it.dataAssertNoErrors.createBackup.url
|
||||||
val response = http.get(
|
val response = http.value.get(
|
||||||
Url("$serverUrl$url"),
|
Url("$serverUrl$url"),
|
||||||
)
|
)
|
||||||
val fileName = response.headers["content-disposition"]!!
|
val fileName = response.headers["content-disposition"]!!
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.data.category
|
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.AddMangaToCategoriesMutation
|
||||||
import ca.gosyer.jui.data.graphql.CreateCategoryMutation
|
import ca.gosyer.jui.data.graphql.CreateCategoryMutation
|
||||||
import ca.gosyer.jui.data.graphql.DeleteCategoryMutation
|
import ca.gosyer.jui.data.graphql.DeleteCategoryMutation
|
||||||
@@ -29,10 +30,13 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class CategoryRepositoryImpl(
|
class CategoryRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : CategoryRepository {
|
) : CategoryRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getMangaCategories(mangaId: Long): Flow<List<Category>> =
|
override fun getMangaCategories(mangaId: Long): Flow<List<Category>> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
GetMangaCategoriesQuery(mangaId.toInt()),
|
GetMangaCategoriesQuery(mangaId.toInt()),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ca.gosyer.jui.data.chapter
|
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.DeleteDownloadedChapterMutation
|
||||||
import ca.gosyer.jui.data.graphql.DeleteDownloadedChaptersMutation
|
import ca.gosyer.jui.data.graphql.DeleteDownloadedChaptersMutation
|
||||||
import ca.gosyer.jui.data.graphql.FetchChapterPagesMutation
|
import ca.gosyer.jui.data.graphql.FetchChapterPagesMutation
|
||||||
@@ -29,10 +30,13 @@ import kotlinx.coroutines.flow.flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class ChapterRepositoryImpl(
|
class ChapterRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : ChapterRepository {
|
) : ChapterRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getChapter(chapterId: Long): Flow<Chapter> =
|
override fun getChapter(chapterId: Long): Flow<Chapter> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
GetChapterQuery(chapterId.toInt()),
|
GetChapterQuery(chapterId.toInt()),
|
||||||
@@ -164,7 +168,7 @@ class ChapterRepositoryImpl(
|
|||||||
): Flow<ByteArray> {
|
): Flow<ByteArray> {
|
||||||
val realUrl = Url("$serverUrl$url")
|
val realUrl = Url("$serverUrl$url")
|
||||||
|
|
||||||
return flow { emit(http.get(realUrl, block).readRawBytes()) }
|
return flow { emit(http.value.get(realUrl, block).readRawBytes()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -6,25 +6,44 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.data.download
|
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.ClearDownloaderMutation
|
||||||
import ca.gosyer.jui.data.graphql.DequeueChapterDownloadMutation
|
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.EnqueueChapterDownloadMutation
|
||||||
import ca.gosyer.jui.data.graphql.EnqueueChapterDownloadsMutation
|
import ca.gosyer.jui.data.graphql.EnqueueChapterDownloadsMutation
|
||||||
import ca.gosyer.jui.data.graphql.ReorderChapterDownloadMutation
|
import ca.gosyer.jui.data.graphql.ReorderChapterDownloadMutation
|
||||||
import ca.gosyer.jui.data.graphql.StartDownloaderMutation
|
import ca.gosyer.jui.data.graphql.StartDownloaderMutation
|
||||||
import ca.gosyer.jui.data.graphql.StopDownloaderMutation
|
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.download.service.DownloadRepository
|
||||||
import ca.gosyer.jui.domain.server.Http
|
import ca.gosyer.jui.domain.server.Http
|
||||||
import com.apollographql.apollo.ApolloClient
|
import com.apollographql.apollo.ApolloClient
|
||||||
import io.ktor.http.Url
|
import io.ktor.http.Url
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
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(
|
class DownloadRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : DownloadRepository {
|
) : DownloadRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun startDownloading(): Flow<Unit> =
|
override fun startDownloading(): Flow<Unit> =
|
||||||
apolloClient.mutation(
|
apolloClient.mutation(
|
||||||
StartDownloaderMutation(),
|
StartDownloaderMutation(),
|
||||||
@@ -90,4 +109,82 @@ class DownloadRepositoryImpl(
|
|||||||
.map {
|
.map {
|
||||||
it.dataAssertNoErrors
|
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
|
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.FetchExtensionsMutation
|
||||||
import ca.gosyer.jui.data.graphql.InstallExtensionMutation
|
import ca.gosyer.jui.data.graphql.InstallExtensionMutation
|
||||||
import ca.gosyer.jui.data.graphql.InstallExternalExtensionMutation
|
import ca.gosyer.jui.data.graphql.InstallExternalExtensionMutation
|
||||||
@@ -23,10 +24,13 @@ import kotlinx.coroutines.flow.map
|
|||||||
import okio.Source
|
import okio.Source
|
||||||
|
|
||||||
class ExtensionRepositoryImpl(
|
class ExtensionRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : ExtensionRepository {
|
) : ExtensionRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getExtensionList(): Flow<List<Extension>> =
|
override fun getExtensionList(): Flow<List<Extension>> =
|
||||||
apolloClient.mutation(
|
apolloClient.mutation(
|
||||||
FetchExtensionsMutation(),
|
FetchExtensionsMutation(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.data.global
|
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.GetGlobalMetaQuery
|
||||||
import ca.gosyer.jui.data.graphql.SetGlobalMetaMutation
|
import ca.gosyer.jui.data.graphql.SetGlobalMetaMutation
|
||||||
import ca.gosyer.jui.domain.global.model.GlobalMeta
|
import ca.gosyer.jui.domain.global.model.GlobalMeta
|
||||||
@@ -17,10 +18,13 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class GlobalRepositoryImpl(
|
class GlobalRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : GlobalRepository {
|
) : GlobalRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getGlobalMeta(): Flow<GlobalMeta> =
|
override fun getGlobalMeta(): Flow<GlobalMeta> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
GetGlobalMetaQuery(),
|
GetGlobalMetaQuery(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ca.gosyer.jui.data.library
|
package ca.gosyer.jui.data.library
|
||||||
|
|
||||||
|
import ca.gosyer.jui.data.ApolloAppClient
|
||||||
import ca.gosyer.jui.data.graphql.SetMangaInLibraryMutation
|
import ca.gosyer.jui.data.graphql.SetMangaInLibraryMutation
|
||||||
import ca.gosyer.jui.domain.library.service.LibraryRepository
|
import ca.gosyer.jui.domain.library.service.LibraryRepository
|
||||||
import ca.gosyer.jui.domain.server.Http
|
import ca.gosyer.jui.domain.server.Http
|
||||||
@@ -9,10 +10,13 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class LibraryRepositoryImpl(
|
class LibraryRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : LibraryRepository {
|
) : LibraryRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
fun setMangaInLibrary(
|
fun setMangaInLibrary(
|
||||||
mangaId: Long,
|
mangaId: Long,
|
||||||
inLibrary: Boolean,
|
inLibrary: Boolean,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ca.gosyer.jui.data.manga
|
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.GetMangaLibraryQuery
|
||||||
import ca.gosyer.jui.data.graphql.GetMangaQuery
|
import ca.gosyer.jui.data.graphql.GetMangaQuery
|
||||||
import ca.gosyer.jui.data.graphql.GetThumbnailUrlQuery
|
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
|
import ca.gosyer.jui.data.graphql.type.UpdateStrategy as GqlUpdateStrategy
|
||||||
|
|
||||||
class MangaRepositoryImpl(
|
class MangaRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : MangaRepository {
|
) : MangaRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getManga(mangaId: Long): Flow<Manga> =
|
override fun getManga(mangaId: Long): Flow<Manga> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
GetMangaQuery(mangaId.toInt()),
|
GetMangaQuery(mangaId.toInt()),
|
||||||
@@ -70,7 +74,7 @@ class MangaRepositoryImpl(
|
|||||||
.toFlow()
|
.toFlow()
|
||||||
.map {
|
.map {
|
||||||
val data = it.dataAssertNoErrors
|
val data = it.dataAssertNoErrors
|
||||||
http.get(data.manga.thumbnailUrl!!).bodyAsChannel()
|
http.value.get(data.manga.thumbnailUrl!!).bodyAsChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateMangaMeta(
|
override fun updateMangaMeta(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.data.settings
|
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.AboutServerQuery
|
||||||
import ca.gosyer.jui.data.graphql.AllSettingsQuery
|
import ca.gosyer.jui.data.graphql.AllSettingsQuery
|
||||||
import ca.gosyer.jui.data.graphql.SetSettingsMutation
|
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
|
import ca.gosyer.jui.domain.settings.model.WebUIInterface as DomainWebUIInterface
|
||||||
|
|
||||||
class SettingsRepositoryImpl(
|
class SettingsRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
) : SettingsRepository {
|
) : SettingsRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
private fun SettingsTypeFragment.toSettings() =
|
private fun SettingsTypeFragment.toSettings() =
|
||||||
Settings(
|
Settings(
|
||||||
authMode = authMode.toDomain(),
|
authMode = authMode.toDomain(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ca.gosyer.jui.data.source
|
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.FetchLatestMangaMutation
|
||||||
import ca.gosyer.jui.data.graphql.FetchPopularMangaMutation
|
import ca.gosyer.jui.data.graphql.FetchPopularMangaMutation
|
||||||
import ca.gosyer.jui.data.graphql.FetchSearchMangaMutation
|
import ca.gosyer.jui.data.graphql.FetchSearchMangaMutation
|
||||||
@@ -33,10 +34,13 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class SourceRepositoryImpl(
|
class SourceRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : SourceRepository {
|
) : SourceRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getSourceList(): Flow<List<Source>> =
|
override fun getSourceList(): Flow<List<Source>> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
GetSourceListQuery(),
|
GetSourceListQuery(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.data.updates
|
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.chapter.ChapterRepositoryImpl.Companion.toMangaAndChapter
|
||||||
import ca.gosyer.jui.data.graphql.GetChapterUpdatesQuery
|
import ca.gosyer.jui.data.graphql.GetChapterUpdatesQuery
|
||||||
import ca.gosyer.jui.data.graphql.UpdateCategoryMutation
|
import ca.gosyer.jui.data.graphql.UpdateCategoryMutation
|
||||||
@@ -19,10 +20,13 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class UpdatesRepositoryImpl(
|
class UpdatesRepositoryImpl(
|
||||||
private val apolloClient: ApolloClient,
|
private val apolloAppClient: ApolloAppClient,
|
||||||
private val http: Http,
|
private val http: Http,
|
||||||
private val serverUrl: Url,
|
private val serverUrl: Url,
|
||||||
) : UpdatesRepository {
|
) : UpdatesRepository {
|
||||||
|
val apolloClient: ApolloClient
|
||||||
|
get() = apolloAppClient.value
|
||||||
|
|
||||||
override fun getRecentUpdates(pageNum: Int): Flow<Updates> =
|
override fun getRecentUpdates(pageNum: Int): Flow<Updates> =
|
||||||
apolloClient.query(
|
apolloClient.query(
|
||||||
GetChapterUpdatesQuery(50, pageNum * 50),
|
GetChapterUpdatesQuery(50, pageNum * 50),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ suspend fun main() {
|
|||||||
serverService.initialized
|
serverService.initialized
|
||||||
.filter { it == ServerResult.STARTED || it == ServerResult.UNUSED }
|
.filter { it == ServerResult.STARTED || it == ServerResult.UNUSED }
|
||||||
.onEach {
|
.onEach {
|
||||||
appComponent.downloadService.init()
|
appComponent.downloadService.getSubscription().launchIn(GlobalScope)
|
||||||
appComponent.libraryUpdateService.init()
|
appComponent.libraryUpdateService.init()
|
||||||
}
|
}
|
||||||
.launchIn(GlobalScope)
|
.launchIn(GlobalScope)
|
||||||
|
|||||||
@@ -118,11 +118,6 @@ interface SharedDomainComponent : CoreComponent {
|
|||||||
val libraryUpdateServiceFactory: LibraryUpdateService
|
val libraryUpdateServiceFactory: LibraryUpdateService
|
||||||
get() = LibraryUpdateService(serverPreferences, http)
|
get() = LibraryUpdateService(serverPreferences, http)
|
||||||
|
|
||||||
@get:AppScope
|
|
||||||
@get:Provides
|
|
||||||
val downloadServiceFactory: DownloadService
|
|
||||||
get() = DownloadService(serverPreferences, http)
|
|
||||||
|
|
||||||
@get:AppScope
|
@get:AppScope
|
||||||
@get:Provides
|
@get:Provides
|
||||||
val serverListenersFactory: ServerListeners
|
val serverListenersFactory: ServerListeners
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ abstract class WebsocketService(
|
|||||||
fun init() {
|
fun init() {
|
||||||
errorConnectionCount = 0
|
errorConnectionCount = 0
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
job = serverUrl
|
job = client
|
||||||
.mapLatest { serverUrl ->
|
.mapLatest { client ->
|
||||||
status.value = Status.STARTING
|
status.value = Status.STARTING
|
||||||
while (true) {
|
while (true) {
|
||||||
if (errorConnectionCount > 3) {
|
if (errorConnectionCount > 3) {
|
||||||
@@ -54,6 +54,7 @@ abstract class WebsocketService(
|
|||||||
throw CancellationException("Finish")
|
throw CancellationException("Finish")
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
|
val serverUrl = serverUrl.value
|
||||||
client.ws(
|
client.ws(
|
||||||
host = serverUrl.host,
|
host = serverUrl.host,
|
||||||
port = serverUrl.port,
|
port = serverUrl.port,
|
||||||
|
|||||||
@@ -6,19 +6,24 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.download.model
|
package ca.gosyer.jui.domain.download.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
data class DownloadQueueItem(
|
||||||
import ca.gosyer.jui.domain.chapter.model.Chapter
|
val position: Int,
|
||||||
import ca.gosyer.jui.domain.manga.model.Manga
|
val progress: Float,
|
||||||
import kotlinx.serialization.Serializable
|
val state: DownloadState,
|
||||||
|
val tries: Int,
|
||||||
@Serializable
|
val chapter: DownloadChapter,
|
||||||
@Immutable
|
val manga: DownloadManga
|
||||||
data class DownloadChapter(
|
)
|
||||||
val chapterIndex: Int,
|
|
||||||
val mangaId: Long,
|
data class DownloadChapter(
|
||||||
val chapter: Chapter,
|
val id: Long,
|
||||||
val manga: Manga,
|
val name: String,
|
||||||
val state: DownloadState = DownloadState.Queued,
|
val pageCount: Int,
|
||||||
val progress: Float = 0f,
|
)
|
||||||
val tries: Int = 0,
|
|
||||||
|
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
|
package ca.gosyer.jui.domain.download.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
enum class DownloadState {
|
||||||
import kotlinx.serialization.Serializable
|
QUEUED,
|
||||||
|
DOWNLOADING,
|
||||||
@Serializable
|
FINISHED,
|
||||||
@Stable
|
ERROR
|
||||||
enum class DownloadState(
|
|
||||||
val state: Int,
|
|
||||||
) {
|
|
||||||
Queued(0),
|
|
||||||
Downloading(1),
|
|
||||||
Finished(2),
|
|
||||||
Error(3),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.download.model
|
package ca.gosyer.jui.domain.download.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@Immutable
|
|
||||||
data class DownloadStatus(
|
data class DownloadStatus(
|
||||||
val status: DownloaderStatus,
|
val status: DownloaderState,
|
||||||
val queue: List<DownloadChapter>,
|
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
|
package ca.gosyer.jui.domain.download.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
enum class DownloaderState {
|
||||||
import kotlinx.serialization.Serializable
|
STARTED,
|
||||||
|
STOPPED
|
||||||
@Serializable
|
|
||||||
@Stable
|
|
||||||
enum class DownloaderStatus {
|
|
||||||
Started,
|
|
||||||
Stopped,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.domain.download.service
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface DownloadRepository {
|
interface DownloadRepository {
|
||||||
@@ -25,4 +27,8 @@ interface DownloadRepository {
|
|||||||
): Flow<Unit>
|
): Flow<Unit>
|
||||||
|
|
||||||
fun batchDownload(chapterIds: List<Long>): 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
|
package ca.gosyer.jui.domain.download.service
|
||||||
|
|
||||||
import ca.gosyer.jui.domain.base.WebsocketService
|
import ca.gosyer.jui.domain.base.WebsocketService
|
||||||
import ca.gosyer.jui.domain.download.model.DownloadChapter
|
import ca.gosyer.jui.domain.download.model.DownloadQueueItem
|
||||||
import ca.gosyer.jui.domain.download.model.DownloadStatus
|
import ca.gosyer.jui.domain.download.model.DownloadUpdateType
|
||||||
import ca.gosyer.jui.domain.download.model.DownloaderStatus
|
import ca.gosyer.jui.domain.download.model.DownloaderState
|
||||||
import ca.gosyer.jui.domain.server.Http
|
import com.diamondedge.logging.logging
|
||||||
import ca.gosyer.jui.domain.server.service.ServerPreferences
|
import kotlinx.coroutines.flow.Flow
|
||||||
import io.ktor.websocket.Frame
|
|
||||||
import io.ktor.websocket.readText
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import me.tatarka.inject.annotations.Inject
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class DownloadService(
|
class DownloadService(
|
||||||
serverPreferences: ServerPreferences,
|
private val downloadRepository: DownloadRepository,
|
||||||
client: Http,
|
) {
|
||||||
) : WebsocketService(serverPreferences, client) {
|
private val log = logging()
|
||||||
override val status: MutableStateFlow<Status>
|
|
||||||
get() = DownloadService.status
|
|
||||||
|
|
||||||
override val query: String
|
fun getSubscription(): Flow<Unit> {
|
||||||
get() = "/api/v1/downloads"
|
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 -> {
|
||||||
downloadQueue.value = status.queue
|
// 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 {
|
companion object {
|
||||||
val status = MutableStateFlow(Status.STARTING)
|
val status = MutableStateFlow(WebsocketService.Status.STARTING)
|
||||||
val downloadQueue = MutableStateFlow(emptyList<DownloadChapter>())
|
val downloadQueue = MutableStateFlow(emptyList<DownloadQueueItem>())
|
||||||
val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
|
val downloaderStatus = MutableStateFlow(DownloaderState.STOPPED)
|
||||||
|
|
||||||
fun registerWatch(mangaId: Long) =
|
fun registerWatch(mangaId: Long) =
|
||||||
downloadQueue
|
downloadQueue
|
||||||
.map {
|
.map {
|
||||||
it.filter { it.mangaId == mangaId }
|
it.filter { it.manga.id == mangaId }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerWatches(mangaIds: Set<Long>) =
|
fun registerWatches(mangaIds: Set<Long>) =
|
||||||
downloadQueue
|
downloadQueue
|
||||||
.map {
|
.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.client.plugins.websocket.WebSockets
|
||||||
import io.ktor.http.URLBuilder
|
import io.ktor.http.URLBuilder
|
||||||
import io.ktor.http.URLProtocol
|
import io.ktor.http.URLProtocol
|
||||||
|
import io.ktor.http.Url
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
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 kotlinx.serialization.json.Json
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import io.ktor.client.plugins.auth.Auth as AuthPlugin
|
import io.ktor.client.plugins.auth.Auth as AuthPlugin
|
||||||
|
|
||||||
typealias Http = HttpClient
|
typealias Http = StateFlow<HttpClient>
|
||||||
|
|
||||||
expect val Engine: HttpClientEngineFactory<HttpClientEngineConfig>
|
expect val Engine: HttpClientEngineFactory<HttpClientEngineConfig>
|
||||||
|
|
||||||
expect fun HttpClientConfig<HttpClientEngineConfig>.configurePlatform()
|
expect fun HttpClientConfig<HttpClientEngineConfig>.configurePlatform()
|
||||||
|
|
||||||
fun httpClient(
|
private fun getHttpClient(
|
||||||
serverPreferences: ServerPreferences,
|
serverUrl: Url,
|
||||||
json: Json,
|
proxy: Proxy,
|
||||||
): Http =
|
proxyHttpHost: String,
|
||||||
HttpClient(Engine) {
|
proxyHttpPort: Int,
|
||||||
|
proxySocksHost: String,
|
||||||
|
proxySocksPort: Int,
|
||||||
|
auth: Auth,
|
||||||
|
authUsername: String,
|
||||||
|
authPassword: String,
|
||||||
|
json: Json
|
||||||
|
): HttpClient {
|
||||||
|
return HttpClient(Engine) {
|
||||||
configurePlatform()
|
configurePlatform()
|
||||||
|
|
||||||
expectSuccess = true
|
expectSuccess = true
|
||||||
|
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
url(serverPreferences.serverUrl().get().toString())
|
url(serverUrl.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
engine {
|
engine {
|
||||||
proxy = when (serverPreferences.proxy().get()) {
|
this.proxy = when (proxy) {
|
||||||
Proxy.NO_PROXY -> null
|
Proxy.NO_PROXY -> null
|
||||||
|
|
||||||
Proxy.HTTP_PROXY -> ProxyBuilder.http(
|
Proxy.HTTP_PROXY -> ProxyBuilder.http(
|
||||||
URLBuilder(
|
URLBuilder(
|
||||||
host = serverPreferences.proxyHttpHost().get(),
|
host = proxyHttpHost,
|
||||||
port = serverPreferences.proxyHttpPort().get(),
|
port = proxyHttpPort,
|
||||||
).build(),
|
).build(),
|
||||||
)
|
)
|
||||||
|
|
||||||
Proxy.SOCKS_PROXY -> ProxyBuilder.socks(
|
Proxy.SOCKS_PROXY -> ProxyBuilder.socks(
|
||||||
serverPreferences.proxySocksHost().get(),
|
proxySocksHost,
|
||||||
serverPreferences.proxySocksPort().get(),
|
proxySocksPort,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (serverPreferences.auth().get()) {
|
when (auth) {
|
||||||
Auth.NONE -> Unit
|
Auth.NONE -> Unit
|
||||||
|
|
||||||
Auth.BASIC -> AuthPlugin {
|
Auth.BASIC -> AuthPlugin {
|
||||||
@@ -81,8 +96,8 @@ fun httpClient(
|
|||||||
}
|
}
|
||||||
credentials {
|
credentials {
|
||||||
BasicAuthCredentials(
|
BasicAuthCredentials(
|
||||||
serverPreferences.authUsername().get(),
|
authUsername,
|
||||||
serverPreferences.authPassword().get(),
|
authPassword,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +107,8 @@ fun httpClient(
|
|||||||
digest {
|
digest {
|
||||||
credentials {
|
credentials {
|
||||||
DigestAuthCredentials(
|
DigestAuthCredentials(
|
||||||
serverPreferences.authUsername().get(),
|
authUsername,
|
||||||
serverPreferences.authPassword().get(),
|
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) =
|
fun asFlow(manualFetch: Boolean) =
|
||||||
flow {
|
flow {
|
||||||
if (!manualFetch && !updatePreferences.enabled().get()) return@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",
|
"https://api.github.com/repos/$GITHUB_REPO/releases/latest",
|
||||||
).body<GithubRelease>()
|
).body<GithubRelease>()
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ actual fun ComponentRegistryBuilder.register(
|
|||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
http: Http,
|
http: Http,
|
||||||
) {
|
) {
|
||||||
setupDefaultComponents(contextWrapper, httpClient = { http })
|
setupDefaultComponents(contextWrapper, httpClient = { http.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun DiskCacheBuilder.configure(
|
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.interactor.DeleteChapterDownload
|
||||||
import ca.gosyer.jui.domain.chapter.model.Chapter
|
import ca.gosyer.jui.domain.chapter.model.Chapter
|
||||||
import ca.gosyer.jui.domain.download.interactor.StopChapterDownload
|
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.download.model.DownloadState
|
||||||
import ca.gosyer.jui.domain.manga.model.Manga
|
import ca.gosyer.jui.domain.manga.model.Manga
|
||||||
import ca.gosyer.jui.i18n.MR
|
import ca.gosyer.jui.i18n.MR
|
||||||
@@ -61,12 +61,12 @@ data class ChapterDownloadItem(
|
|||||||
)
|
)
|
||||||
val downloadState = _downloadState.asStateFlow()
|
val downloadState = _downloadState.asStateFlow()
|
||||||
|
|
||||||
private val _downloadChapterFlow: MutableStateFlow<DownloadChapter?> = MutableStateFlow(null)
|
private val _downloadChapterFlow: MutableStateFlow<DownloadQueueItem?> = MutableStateFlow(null)
|
||||||
val downloadChapterFlow = _downloadChapterFlow.asStateFlow()
|
val downloadChapterFlow = _downloadChapterFlow.asStateFlow()
|
||||||
|
|
||||||
fun updateFrom(downloadingChapters: List<DownloadChapter>) {
|
fun updateFrom(downloadingChapters: List<DownloadQueueItem>) {
|
||||||
val downloadingChapter = downloadingChapters.find {
|
val downloadingChapter = downloadingChapters.find {
|
||||||
it.chapterIndex == chapter.index && it.mangaId == chapter.mangaId
|
it.chapter.id == chapter.id
|
||||||
}
|
}
|
||||||
if (downloadingChapter != null && downloadState.value != ChapterDownloadState.Downloading) {
|
if (downloadingChapter != null && downloadState.value != ChapterDownloadState.Downloading) {
|
||||||
_downloadState.value = ChapterDownloadState.Downloading
|
_downloadState.value = ChapterDownloadState.Downloading
|
||||||
@@ -155,11 +155,11 @@ private fun DownloadIconButton(onClick: () -> Unit) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DownloadingIconButton(
|
private fun DownloadingIconButton(
|
||||||
downloadChapter: DownloadChapter?,
|
downloadChapter: DownloadQueueItem?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
DropdownIconButton(
|
DropdownIconButton(
|
||||||
downloadChapter?.mangaId to downloadChapter?.chapterIndex,
|
downloadChapter?.chapter?.id,
|
||||||
{
|
{
|
||||||
DropdownMenuItem(onClick = onClick) {
|
DropdownMenuItem(onClick = onClick) {
|
||||||
Text(stringResource(MR.strings.action_cancel))
|
Text(stringResource(MR.strings.action_cancel))
|
||||||
@@ -167,7 +167,7 @@ private fun DownloadingIconButton(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
when (downloadChapter?.state) {
|
when (downloadChapter?.state) {
|
||||||
null, DownloadState.Queued -> CircularProgressIndicator(
|
null, DownloadState.QUEUED -> CircularProgressIndicator(
|
||||||
Modifier
|
Modifier
|
||||||
.size(26.dp)
|
.size(26.dp)
|
||||||
.padding(2.dp),
|
.padding(2.dp),
|
||||||
@@ -175,7 +175,7 @@ private fun DownloadingIconButton(
|
|||||||
2.dp,
|
2.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
DownloadState.Downloading -> if (downloadChapter.progress != 0.0F) {
|
DownloadState.DOWNLOADING -> if (downloadChapter.progress != 0.0F) {
|
||||||
val animatedProgress by animateFloatAsState(
|
val animatedProgress by animateFloatAsState(
|
||||||
targetValue = downloadChapter.progress,
|
targetValue = downloadChapter.progress,
|
||||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
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(
|
Icon(
|
||||||
Icons.Rounded.Error,
|
Icons.Rounded.Error,
|
||||||
null,
|
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(
|
Icon(
|
||||||
Icons.Rounded.Check,
|
Icons.Rounded.Check,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
package ca.gosyer.jui.ui.base.image
|
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.extension.model.Extension
|
||||||
import ca.gosyer.jui.domain.manga.model.Manga
|
import ca.gosyer.jui.domain.manga.model.Manga
|
||||||
import ca.gosyer.jui.domain.server.Http
|
import ca.gosyer.jui.domain.server.Http
|
||||||
@@ -42,10 +43,13 @@ class ImageLoaderProvider(
|
|||||||
fun get(imageCache: ImageCache): ImageLoader =
|
fun get(imageCache: ImageCache): ImageLoader =
|
||||||
ImageLoader {
|
ImageLoader {
|
||||||
components {
|
components {
|
||||||
|
add(KtorUrlFetcher.Factory { http.value })
|
||||||
register(context, http)
|
register(context, http)
|
||||||
add(MokoResourceFetcher.Factory())
|
add(MokoResourceFetcher.Factory())
|
||||||
add(MangaCoverMapper())
|
add(MangaCoverMapper())
|
||||||
add(MangaCoverKeyer())
|
add(MangaCoverKeyer())
|
||||||
|
add(DownloadMangaCoverMapper())
|
||||||
|
add(DownloadMangaCoverKeyer())
|
||||||
add(ExtensionIconMapper())
|
add(ExtensionIconMapper())
|
||||||
add(ExtensionIconKeyer())
|
add(ExtensionIconKeyer())
|
||||||
add(SourceIconMapper())
|
add(SourceIconMapper())
|
||||||
@@ -77,7 +81,28 @@ class ImageLoaderProvider(
|
|||||||
options: Options,
|
options: Options,
|
||||||
): String? {
|
): String? {
|
||||||
if (data !is Manga) return null
|
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
|
package ca.gosyer.jui.ui.downloads
|
||||||
|
|
||||||
import ca.gosyer.jui.domain.base.WebsocketService.Actions
|
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.ClearDownloadQueue
|
||||||
import ca.gosyer.jui.domain.download.interactor.QueueChapterDownload
|
import ca.gosyer.jui.domain.download.interactor.QueueChapterDownload
|
||||||
import ca.gosyer.jui.domain.download.interactor.ReorderChapterDownload
|
import ca.gosyer.jui.domain.download.interactor.ReorderChapterDownload
|
||||||
import ca.gosyer.jui.domain.download.interactor.StartDownloading
|
import ca.gosyer.jui.domain.download.interactor.StartDownloading
|
||||||
import ca.gosyer.jui.domain.download.interactor.StopChapterDownload
|
import ca.gosyer.jui.domain.download.interactor.StopChapterDownload
|
||||||
import ca.gosyer.jui.domain.download.interactor.StopDownloading
|
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.domain.download.service.DownloadService
|
||||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||||
@@ -69,35 +69,35 @@ class DownloadsScreenViewModel(
|
|||||||
scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) }
|
scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopDownload(chapter: Chapter) {
|
fun stopDownload(chapter: DownloadChapter) {
|
||||||
scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) }
|
scope.launch { stopChapterDownload.await(chapter.id, onError = { toast(it.message.orEmpty()) }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveUp(chapter: Chapter) {
|
fun moveUp(chapter: DownloadChapter) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val index =
|
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
|
if (index == -1 || index <= 0) return@launch
|
||||||
reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) })
|
reorderChapterDownload.await(chapter.id, index - 1, onError = { toast(it.message.orEmpty()) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveDown(chapter: Chapter) {
|
fun moveDown(chapter: DownloadChapter) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val index =
|
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
|
if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch
|
||||||
reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) })
|
reorderChapterDownload.await(chapter.id, index + 1, onError = { toast(it.message.orEmpty()) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToTop(chapter: Chapter) {
|
fun moveToTop(chapter: DownloadChapter) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) })
|
reorderChapterDownload.await(chapter.id, 0, onError = { toast(it.message.orEmpty()) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToBottom(chapter: Chapter) {
|
fun moveToBottom(chapter: DownloadChapter) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
reorderChapterDownload.await(
|
reorderChapterDownload.await(
|
||||||
chapter.id,
|
chapter.id,
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.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.i18n.MR
|
||||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||||
@@ -71,17 +71,17 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsScreenContent(
|
fun DownloadsScreenContent(
|
||||||
downloadQueue: ImmutableList<DownloadChapter>,
|
downloadQueue: ImmutableList<DownloadQueueItem>,
|
||||||
downloadStatus: DownloaderStatus,
|
downloadStatus: DownloaderState,
|
||||||
startDownloading: () -> Unit,
|
startDownloading: () -> Unit,
|
||||||
pauseDownloading: () -> Unit,
|
pauseDownloading: () -> Unit,
|
||||||
clearQueue: () -> Unit,
|
clearQueue: () -> Unit,
|
||||||
onMangaClick: (Long) -> Unit,
|
onMangaClick: (Long) -> Unit,
|
||||||
stopDownload: (Chapter) -> Unit,
|
stopDownload: (DownloadChapter) -> Unit,
|
||||||
moveDownloadUp: (Chapter) -> Unit,
|
moveDownloadUp: (DownloadChapter) -> Unit,
|
||||||
moveDownloadDown: (Chapter) -> Unit,
|
moveDownloadDown: (DownloadChapter) -> Unit,
|
||||||
moveDownloadToTop: (Chapter) -> Unit,
|
moveDownloadToTop: (DownloadChapter) -> Unit,
|
||||||
moveDownloadToBottom: (Chapter) -> Unit,
|
moveDownloadToBottom: (DownloadChapter) -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.windowInsetsPadding(
|
modifier = Modifier.windowInsetsPadding(
|
||||||
@@ -114,11 +114,11 @@ fun DownloadsScreenContent(
|
|||||||
),
|
),
|
||||||
).asPaddingValues(),
|
).asPaddingValues(),
|
||||||
) {
|
) {
|
||||||
items(downloadQueue, key = { "${it.mangaId}-${it.chapterIndex}" }) {
|
items(downloadQueue, key = { it.chapter.id }) {
|
||||||
DownloadsItem(
|
DownloadsItem(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
item = it,
|
item = it,
|
||||||
onClickCover = { onMangaClick(it.mangaId) },
|
onClickCover = { onMangaClick(it.manga.id) },
|
||||||
onClickCancel = stopDownload,
|
onClickCancel = stopDownload,
|
||||||
onClickMoveUp = moveDownloadUp,
|
onClickMoveUp = moveDownloadUp,
|
||||||
onClickMoveDown = moveDownloadDown,
|
onClickMoveDown = moveDownloadDown,
|
||||||
@@ -147,13 +147,13 @@ fun DownloadsScreenContent(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DownloadsItem(
|
fun DownloadsItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: DownloadChapter,
|
item: DownloadQueueItem,
|
||||||
onClickCover: () -> Unit,
|
onClickCover: () -> Unit,
|
||||||
onClickCancel: (Chapter) -> Unit,
|
onClickCancel: (DownloadChapter) -> Unit,
|
||||||
onClickMoveUp: (Chapter) -> Unit,
|
onClickMoveUp: (DownloadChapter) -> Unit,
|
||||||
onClickMoveDown: (Chapter) -> Unit,
|
onClickMoveDown: (DownloadChapter) -> Unit,
|
||||||
onClickMoveToTop: (Chapter) -> Unit,
|
onClickMoveToTop: (DownloadChapter) -> Unit,
|
||||||
onClickMoveToBottom: (Chapter) -> Unit,
|
onClickMoveToBottom: (DownloadChapter) -> Unit,
|
||||||
) {
|
) {
|
||||||
MangaListItem(
|
MangaListItem(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -180,8 +180,8 @@ fun DownloadsItem(
|
|||||||
text = item.manga.title,
|
text = item.manga.title,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
val progress = if (item.chapter.pageCount != null && item.chapter.pageCount != -1) {
|
val progress = if (item.chapter.pageCount > 0) {
|
||||||
" - " + "${(item.chapter.pageCount!! * item.progress).toInt()}/${item.chapter.pageCount}"
|
" - " + "${(item.chapter.pageCount * item.progress).toInt()}/${item.chapter.pageCount}"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ fun DownloadsItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownIconButton(
|
DropdownIconButton(
|
||||||
item.mangaId to item.chapterIndex,
|
item.chapter.id,
|
||||||
{
|
{
|
||||||
DropdownMenuItem(onClick = { onClickCancel(item.chapter) }) {
|
DropdownMenuItem(onClick = { onClickCancel(item.chapter) }) {
|
||||||
Text(stringResource(MR.strings.action_cancel))
|
Text(stringResource(MR.strings.action_cancel))
|
||||||
@@ -231,13 +231,13 @@ fun DownloadsItem(
|
|||||||
@Stable
|
@Stable
|
||||||
@Composable
|
@Composable
|
||||||
private fun getActionItems(
|
private fun getActionItems(
|
||||||
downloadStatus: DownloaderStatus,
|
downloadStatus: DownloaderState,
|
||||||
startDownloading: () -> Unit,
|
startDownloading: () -> Unit,
|
||||||
pauseDownloading: () -> Unit,
|
pauseDownloading: () -> Unit,
|
||||||
clearQueue: () -> Unit,
|
clearQueue: () -> Unit,
|
||||||
): ImmutableList<ActionItem> =
|
): ImmutableList<ActionItem> =
|
||||||
listOf(
|
listOf(
|
||||||
if (downloadStatus == DownloaderStatus.Started) {
|
if (downloadStatus == DownloaderState.STARTED) {
|
||||||
ActionItem(
|
ActionItem(
|
||||||
stringResource(MR.strings.action_pause),
|
stringResource(MR.strings.action_pause),
|
||||||
Icons.Rounded.Pause,
|
Icons.Rounded.Pause,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import ca.gosyer.jui.domain.base.WebsocketService
|
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.i18n.MR
|
||||||
import ca.gosyer.jui.ui.base.LocalViewModels
|
import ca.gosyer.jui.ui.base.LocalViewModels
|
||||||
import ca.gosyer.jui.uicore.resources.stringResource
|
import ca.gosyer.jui.uicore.resources.stringResource
|
||||||
@@ -43,7 +43,7 @@ fun DownloadsExtraInfo() {
|
|||||||
WebsocketService.Status.RUNNING -> {
|
WebsocketService.Status.RUNNING -> {
|
||||||
if (list.isNotEmpty()) {
|
if (list.isNotEmpty()) {
|
||||||
val remainingDownloads = stringResource(MR.strings.downloads_remaining, list.size)
|
val remainingDownloads = stringResource(MR.strings.downloads_remaining, list.size)
|
||||||
if (downloaderStatus == DownloaderStatus.Stopped) {
|
if (downloaderStatus == DownloaderState.STOPPED) {
|
||||||
stringResource(MR.strings.downloads_paused) + " • " + remainingDownloads
|
stringResource(MR.strings.downloads_paused) + " • " + remainingDownloads
|
||||||
} else {
|
} else {
|
||||||
remainingDownloads
|
remainingDownloads
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class UpdatesScreenViewModel(
|
|||||||
}
|
}
|
||||||
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
.onEach { (mangaIds, queue) ->
|
.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 {
|
updates.value.filterIsInstance<UpdatesUI.Item>().forEach {
|
||||||
it.chapterDownloadItem.updateFrom(chapters)
|
it.chapterDownloadItem.updateFrom(chapters)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ actual fun ComponentRegistryBuilder.register(
|
|||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
http: Http,
|
http: Http,
|
||||||
) {
|
) {
|
||||||
setupDefaultComponents(httpClient = { http })
|
setupDefaultComponents(httpClient = { http.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun DiskCacheBuilder.configure(
|
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.base.WebsocketService
|
||||||
import ca.gosyer.jui.domain.download.service.DownloadService
|
import ca.gosyer.jui.domain.download.service.DownloadService
|
||||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
||||||
internal actual fun startDownloadService(
|
internal actual fun startDownloadService(
|
||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
downloadService: DownloadService,
|
downloadService: DownloadService,
|
||||||
actions: WebsocketService.Actions,
|
actions: WebsocketService.Actions,
|
||||||
) {
|
) {
|
||||||
downloadService.init()
|
downloadService.getSubscription().launchIn(GlobalScope)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ suspend fun imageFromUrl(
|
|||||||
url: String,
|
url: String,
|
||||||
block: HttpRequestBuilder.() -> Unit,
|
block: HttpRequestBuilder.() -> Unit,
|
||||||
): ImageBitmap =
|
): ImageBitmap =
|
||||||
client.get(url) {
|
client.value.get(url) {
|
||||||
expectSuccess = true
|
expectSuccess = true
|
||||||
block()
|
block()
|
||||||
}.toImageBitmap()
|
}.toImageBitmap()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ actual fun ComponentRegistryBuilder.register(
|
|||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
http: Http,
|
http: Http,
|
||||||
) {
|
) {
|
||||||
setupDefaultComponents(httpClient = { http })
|
setupDefaultComponents(httpClient = { http.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun DiskCacheBuilder.configure(
|
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.base.WebsocketService
|
||||||
import ca.gosyer.jui.domain.download.service.DownloadService
|
import ca.gosyer.jui.domain.download.service.DownloadService
|
||||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
||||||
internal actual fun startDownloadService(
|
internal actual fun startDownloadService(
|
||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
downloadService: DownloadService,
|
downloadService: DownloadService,
|
||||||
actions: WebsocketService.Actions,
|
actions: WebsocketService.Actions,
|
||||||
) {
|
) {
|
||||||
downloadService.init()
|
downloadService.getSubscription().launchIn(GlobalScope)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user