mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-22 20:42:34 +01:00
Implement library updater
This commit is contained in:
@@ -37,5 +37,8 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".data.download.AndroidDownloadService"
|
android:name=".data.download.AndroidDownloadService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name=".data.library.AndroidLibraryService"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.DisposableEffect
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import ca.gosyer.jui.android.data.download.AndroidDownloadService
|
import ca.gosyer.jui.android.data.download.AndroidDownloadService
|
||||||
|
import ca.gosyer.jui.android.data.library.AndroidLibraryService
|
||||||
import ca.gosyer.jui.domain.base.WebsocketService.Actions
|
import ca.gosyer.jui.domain.base.WebsocketService.Actions
|
||||||
import ca.gosyer.jui.ui.base.theme.AppTheme
|
import ca.gosyer.jui.ui.base.theme.AppTheme
|
||||||
import ca.gosyer.jui.ui.main.MainMenu
|
import ca.gosyer.jui.ui.main.MainMenu
|
||||||
@@ -33,6 +34,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AndroidDownloadService.start(this, Actions.START)
|
AndroidDownloadService.start(this, Actions.START)
|
||||||
|
AndroidLibraryService.start(this, Actions.START)
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
@@ -71,5 +73,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
AndroidDownloadService.stop(this)
|
AndroidDownloadService.stop(this)
|
||||||
|
AndroidLibraryService.stop(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,11 @@ 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.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.receiveAsFlow
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
@@ -72,6 +75,10 @@ class AndroidDownloadService : Service() {
|
|||||||
return instance != null
|
return instance != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
private val log = logging()
|
private val log = logging()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,14 +91,14 @@ class AndroidDownloadService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, placeholderNotification)
|
startForeground(Notifications.ID_DOWNLOADER_RUNNING, placeholderNotification)
|
||||||
status.value = Status.STARTING
|
status.value = Status.STARTING
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
ioScope.cancel()
|
ioScope.cancel()
|
||||||
status.value = Status.STOPPED
|
status.value = Status.STOPPED
|
||||||
notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
notificationManager.cancel(Notifications.ID_DOWNLOADER_RUNNING)
|
||||||
if (instance == this) {
|
if (instance == this) {
|
||||||
instance = null
|
instance = null
|
||||||
}
|
}
|
||||||
@@ -153,6 +160,9 @@ class AndroidDownloadService : Service() {
|
|||||||
|
|
||||||
incoming.receiveAsFlow()
|
incoming.receiveAsFlow()
|
||||||
.filterIsInstance<Frame.Text>()
|
.filterIsInstance<Frame.Text>()
|
||||||
|
.map { json.decodeFromString<DownloadStatus>(it.readText()) }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.drop(1)
|
||||||
.mapLatest(::onReceived)
|
.mapLatest(::onReceived)
|
||||||
.catch {
|
.catch {
|
||||||
log.warn(it) { "Error running downloader" }
|
log.warn(it) { "Error running downloader" }
|
||||||
@@ -173,8 +183,7 @@ class AndroidDownloadService : Service() {
|
|||||||
.launchIn(ioScope)
|
.launchIn(ioScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onReceived(frame: Frame.Text) {
|
private fun onReceived(status: DownloadStatus) {
|
||||||
val status = Json.decodeFromString<DownloadStatus>(frame.readText())
|
|
||||||
DownloadService.downloaderStatus.value = status.status
|
DownloadService.downloaderStatus.value = status.status
|
||||||
DownloadService.downloadQueue.value = status.queue
|
DownloadService.downloadQueue.value = status.queue
|
||||||
val downloadingChapter = status.queue.lastOrNull { it.state == DownloadState.Downloading }
|
val downloadingChapter = status.queue.lastOrNull { it.state == DownloadState.Downloading }
|
||||||
@@ -199,22 +208,22 @@ class AndroidDownloadService : Service() {
|
|||||||
)
|
)
|
||||||
}.build()
|
}.build()
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
Notifications.ID_DOWNLOAD_CHAPTER,
|
Notifications.ID_DOWNLOADER_DOWNLOADING,
|
||||||
notification
|
notification
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(Notifications.ID_DOWNLOAD_CHAPTER, placeholderNotification)
|
notificationManager.cancel(Notifications.ID_DOWNLOADER_DOWNLOADING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val placeholderNotification by lazy {
|
private val placeholderNotification by lazy {
|
||||||
notification(Notifications.CHANNEL_DOWNLOADER) {
|
notification(Notifications.CHANNEL_DOWNLOADER_RUNNING) {
|
||||||
setContentTitle(MR.strings.downloader_running.desc().toString(this@AndroidDownloadService))
|
setContentTitle(MR.strings.downloader_running.desc().toString(this@AndroidDownloadService))
|
||||||
setSmallIcon(R.drawable.ic_round_get_app_24)
|
setSmallIcon(R.drawable.ic_round_get_app_24)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val progressNotificationBuilder by lazy {
|
private val progressNotificationBuilder by lazy {
|
||||||
notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
notificationBuilder(Notifications.CHANNEL_DOWNLOADER_DOWNLOADING) {
|
||||||
setSmallIcon(R.drawable.ic_round_get_app_24)
|
setSmallIcon(R.drawable.ic_round_get_app_24)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setOnlyAlertOnce(true)
|
setOnlyAlertOnce(true)
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.android.data.library
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ca.gosyer.jui.android.AppComponent
|
||||||
|
import ca.gosyer.jui.android.R
|
||||||
|
import ca.gosyer.jui.android.data.notification.Notifications
|
||||||
|
import ca.gosyer.jui.android.util.notification
|
||||||
|
import ca.gosyer.jui.android.util.notificationBuilder
|
||||||
|
import ca.gosyer.jui.android.util.notificationManager
|
||||||
|
import ca.gosyer.jui.core.lang.chop
|
||||||
|
import ca.gosyer.jui.core.lang.throwIfCancellation
|
||||||
|
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.library.model.JobStatus
|
||||||
|
import ca.gosyer.jui.domain.library.model.UpdateStatus
|
||||||
|
import ca.gosyer.jui.domain.library.service.LibraryUpdateService
|
||||||
|
import ca.gosyer.jui.domain.library.service.LibraryUpdateService.Companion.status
|
||||||
|
import ca.gosyer.jui.i18n.MR
|
||||||
|
import dev.icerock.moko.resources.desc.desc
|
||||||
|
import dev.icerock.moko.resources.format
|
||||||
|
import io.ktor.client.plugins.websocket.ws
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import io.ktor.websocket.readText
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
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.job
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
|
class AndroidLibraryService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var instance: AndroidLibraryService? = null
|
||||||
|
|
||||||
|
fun start(context: Context, actions: Actions) {
|
||||||
|
if (!isRunning() && actions != Actions.STOP) {
|
||||||
|
val intent = Intent(context, AndroidLibraryService::class.java).apply {
|
||||||
|
action = actions.name
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, AndroidLibraryService::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isRunning(): Boolean {
|
||||||
|
return instance != null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val log = logging()
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var ioScope: CoroutineScope
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
startForeground(Notifications.ID_LIBRARY_UPDATES, placeholderNotification)
|
||||||
|
status.value = Status.STARTING
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
ioScope.cancel()
|
||||||
|
status.value = Status.STOPPED
|
||||||
|
notificationManager.cancel(Notifications.ID_LIBRARY_UPDATES)
|
||||||
|
if (instance == this) {
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
instance = this
|
||||||
|
ioScope.coroutineContext.job.cancelChildren()
|
||||||
|
|
||||||
|
if (intent != null) {
|
||||||
|
val action = intent.action
|
||||||
|
log.info { "using an intent with action $action" }
|
||||||
|
when (action) {
|
||||||
|
Actions.START.name,
|
||||||
|
Actions.RESTART.name -> startWebsocket()
|
||||||
|
Actions.STOP.name -> stopSelf()
|
||||||
|
else -> log.info { "This should never happen. No action in the received intent" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info { "with a null intent. It has been probably restarted by the system." }
|
||||||
|
startWebsocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWebsocket() {
|
||||||
|
ioScope.coroutineContext.job.cancelChildren()
|
||||||
|
ioScope.coroutineContext.job.invokeOnCompletion {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
val appComponent = AppComponent.getInstance(applicationContext)
|
||||||
|
val client = appComponent.http
|
||||||
|
|
||||||
|
var errorConnectionCount = 0
|
||||||
|
|
||||||
|
appComponent
|
||||||
|
.serverPreferences
|
||||||
|
.serverUrl()
|
||||||
|
.getAsFlow()
|
||||||
|
.mapLatest { serverUrl ->
|
||||||
|
status.value = Status.STARTING
|
||||||
|
while (true) {
|
||||||
|
if (errorConnectionCount > 3) {
|
||||||
|
status.value = Status.STOPPED
|
||||||
|
throw CancellationException()
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
client.ws(
|
||||||
|
host = serverUrl.host,
|
||||||
|
port = serverUrl.port,
|
||||||
|
path = serverUrl.encodedPath + "/api/v1/update"
|
||||||
|
) {
|
||||||
|
errorConnectionCount = 0
|
||||||
|
status.value = Status.RUNNING
|
||||||
|
send(Frame.Text("STATUS"))
|
||||||
|
|
||||||
|
incoming.receiveAsFlow()
|
||||||
|
.filterIsInstance<Frame.Text>()
|
||||||
|
.map { json.decodeFromString<UpdateStatus>(it.readText()) }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.drop(1)
|
||||||
|
.mapLatest(::onReceived)
|
||||||
|
.catch {
|
||||||
|
log.warn(it) { "Error running library update" }
|
||||||
|
}
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}.throwIfCancellation().isFailure.let {
|
||||||
|
status.value = Status.STARTING
|
||||||
|
if (it) errorConnectionCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.catch {
|
||||||
|
status.value = Status.STOPPED
|
||||||
|
log.warn(it) { "Error while running websocket service" }
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
.launchIn(ioScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReceived(status: UpdateStatus) {
|
||||||
|
LibraryUpdateService.updateStatus.value = status
|
||||||
|
|
||||||
|
val complete = status.statusMap[JobStatus.COMPLETE]?.size ?: 0
|
||||||
|
val failed = status.statusMap[JobStatus.FAILED]?.size ?: 0
|
||||||
|
val running = status.statusMap[JobStatus.RUNNING]?.size ?: 0
|
||||||
|
val pending = status.statusMap[JobStatus.PENDING]?.size ?: 0
|
||||||
|
val total = complete + failed + running + pending
|
||||||
|
val current = complete + failed
|
||||||
|
if (current != total) {
|
||||||
|
val notification = with(progressNotificationBuilder) {
|
||||||
|
val updatingText = status.statusMap[JobStatus.RUNNING]
|
||||||
|
?.joinToString("\n") { it.title.chop(40) }
|
||||||
|
setContentTitle(
|
||||||
|
MR.strings.notification_updating
|
||||||
|
.format(current, total)
|
||||||
|
.toString(this@AndroidLibraryService)
|
||||||
|
)
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
|
setProgress(total, current, false)
|
||||||
|
}.build()
|
||||||
|
notificationManager.notify(
|
||||||
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
|
notification
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val placeholderNotification by lazy {
|
||||||
|
notification(Notifications.CHANNEL_LIBRARY_UPDATES) {
|
||||||
|
setContentTitle(MR.strings.library_updater_running.desc().toString(this@AndroidLibraryService))
|
||||||
|
setSmallIcon(R.drawable.ic_round_get_app_24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val progressNotificationBuilder by lazy {
|
||||||
|
notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
|
setSmallIcon(R.drawable.ic_round_get_app_24)
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setOngoing(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,20 @@ object Notifications {
|
|||||||
* Notification channel and ids used by the downloader.
|
* Notification channel and ids used by the downloader.
|
||||||
*/
|
*/
|
||||||
private const val GROUP_DOWNLOADER = "group_downloader"
|
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
const val CHANNEL_DOWNLOADER_RUNNING = "downloader_channel"
|
||||||
const val ID_DOWNLOAD_CHAPTER = -101
|
const val ID_DOWNLOADER_RUNNING = -101
|
||||||
|
const val CHANNEL_DOWNLOADER_DOWNLOADING = "downloader_channel_downloading"
|
||||||
|
const val ID_DOWNLOADER_DOWNLOADING = -102
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used by the library updates.
|
||||||
|
*/
|
||||||
|
private const val GROUP_LIBRARY = "group_library"
|
||||||
|
const val CHANNEL_LIBRARY_UPDATES = "library_updates_channel"
|
||||||
|
const val ID_LIBRARY_UPDATES = -301
|
||||||
|
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
|
||||||
|
const val ID_LIBRARY_PROGRESS = -302
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used for app updates.
|
* Notification channel and ids used for app updates.
|
||||||
@@ -39,6 +51,9 @@ object Notifications {
|
|||||||
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||||
setName(MR.strings.group_downloader.desc().toString(context))
|
setName(MR.strings.group_downloader.desc().toString(context))
|
||||||
},
|
},
|
||||||
|
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
||||||
|
setName(MR.strings.group_library.desc().toString(context))
|
||||||
|
},
|
||||||
buildNotificationChannelGroup(GROUP_APK_UPDATES) {
|
buildNotificationChannelGroup(GROUP_APK_UPDATES) {
|
||||||
setName(MR.strings.group_updates.desc().toString(context))
|
setName(MR.strings.group_updates.desc().toString(context))
|
||||||
}
|
}
|
||||||
@@ -48,13 +63,37 @@ object Notifications {
|
|||||||
notificationService.createNotificationChannelsCompat(
|
notificationService.createNotificationChannelsCompat(
|
||||||
listOf(
|
listOf(
|
||||||
buildNotificationChannel(
|
buildNotificationChannel(
|
||||||
CHANNEL_DOWNLOADER,
|
CHANNEL_DOWNLOADER_RUNNING,
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
) {
|
) {
|
||||||
setName(MR.strings.group_downloader_channel.desc().toString(context))
|
setName(MR.strings.channel_active.desc().toString(context))
|
||||||
setGroup(GROUP_DOWNLOADER)
|
setGroup(GROUP_DOWNLOADER)
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
},
|
},
|
||||||
|
buildNotificationChannel(
|
||||||
|
CHANNEL_DOWNLOADER_DOWNLOADING,
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
) {
|
||||||
|
setName(MR.strings.channel_progress.desc().toString(context))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(
|
||||||
|
CHANNEL_LIBRARY_UPDATES,
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
) {
|
||||||
|
setName(MR.strings.channel_active.desc().toString(context))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(
|
||||||
|
CHANNEL_LIBRARY_PROGRESS,
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
) {
|
||||||
|
setName(MR.strings.channel_progress.desc().toString(context))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
buildNotificationChannel(
|
buildNotificationChannel(
|
||||||
CHANNEL_APP_UPDATE,
|
CHANNEL_APP_UPDATE,
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ suspend fun main() {
|
|||||||
.filter { it == ServerResult.STARTED || it == ServerResult.UNUSED }
|
.filter { it == ServerResult.STARTED || it == ServerResult.UNUSED }
|
||||||
.onEach {
|
.onEach {
|
||||||
appComponent.downloadService.init()
|
appComponent.downloadService.init()
|
||||||
// dataComponent.libraryUpdateService.init()
|
appComponent.libraryUpdateService.init()
|
||||||
}
|
}
|
||||||
.launchIn(GlobalScope)
|
.launchIn(GlobalScope)
|
||||||
|
|
||||||
|
|||||||
@@ -22,17 +22,20 @@ class LibraryUpdateService @Inject constructor(
|
|||||||
client: Http
|
client: Http
|
||||||
) : WebsocketService(serverPreferences, client) {
|
) : WebsocketService(serverPreferences, client) {
|
||||||
|
|
||||||
override val _status: MutableStateFlow<Status> = MutableStateFlow(Status.STARTING)
|
override val _status: MutableStateFlow<Status>
|
||||||
|
get() = status
|
||||||
|
|
||||||
override val query: String
|
override val query: String
|
||||||
get() = "/api/v1/update"
|
get() = "/api/v1/update"
|
||||||
|
|
||||||
override suspend fun onReceived(frame: Frame.Text) {
|
override suspend fun onReceived(frame: Frame.Text) {
|
||||||
val status = json.decodeFromString<UpdateStatus>(frame.readText())
|
updateStatus.value = json.decodeFromString<UpdateStatus>(frame.readText())
|
||||||
log.info { status }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
companion object {
|
||||||
private val log = logging()
|
private val log = logging()
|
||||||
|
|
||||||
|
val status = MutableStateFlow(Status.STARTING)
|
||||||
|
val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@
|
|||||||
<string name="action_filter_bookmarked">Bookmarked</string>
|
<string name="action_filter_bookmarked">Bookmarked</string>
|
||||||
<string name="action_select_all">Select all</string>
|
<string name="action_select_all">Select all</string>
|
||||||
<string name="action_select_inverse">Select inverse</string>
|
<string name="action_select_inverse">Select inverse</string>
|
||||||
|
<string name="action_update_library">Update library</string>
|
||||||
|
<string name="action_restart_library">Restart library service</string>
|
||||||
|
|
||||||
<!-- Locations -->
|
<!-- Locations -->
|
||||||
<string name="location_library">Library</string>
|
<string name="location_library">Library</string>
|
||||||
@@ -318,10 +320,15 @@
|
|||||||
|
|
||||||
<!-- Android notifications -->
|
<!-- Android notifications -->
|
||||||
<string name="group_downloader">Downloader</string>
|
<string name="group_downloader">Downloader</string>
|
||||||
<string name="group_downloader_channel">Progress</string>
|
<string name="group_library">Library</string>
|
||||||
|
<string name="channel_progress">Progress</string>
|
||||||
|
<string name="channel_downloading">Progress</string>
|
||||||
|
<string name="channel_active">Active</string>
|
||||||
<string name="group_updates">Updates</string>
|
<string name="group_updates">Updates</string>
|
||||||
<string name="group_updates_channel_app">App updates</string>
|
<string name="group_updates_channel_app">App updates</string>
|
||||||
<string name="group_updates_channel_ext">Extension Updates</string>
|
<string name="group_updates_channel_ext">Extension Updates</string>
|
||||||
<string name="downloader_running">Downloader running</string>
|
<string name="downloader_running">Downloader running</string>
|
||||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
<string name="chapter_downloading_progress">Downloading… (%1$d/%2$d)</string>
|
||||||
|
<string name="library_updater_running">Library updater running</string>
|
||||||
|
<string name="notification_updating">Updating library… (%1$d/%2$d)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.ui.main.components
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ca.gosyer.jui.domain.base.WebsocketService
|
||||||
|
import ca.gosyer.jui.domain.library.service.LibraryUpdateService
|
||||||
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
|
|
||||||
|
internal actual fun startLibraryUpdatesService(
|
||||||
|
contextWrapper: ContextWrapper,
|
||||||
|
libraryUpdatesService: LibraryUpdateService,
|
||||||
|
actions: WebsocketService.Actions
|
||||||
|
) {
|
||||||
|
val intent = Intent(
|
||||||
|
contextWrapper,
|
||||||
|
Class.forName("ca.gosyer.jui.android.data.library.AndroidLibraryService")
|
||||||
|
).apply {
|
||||||
|
action = actions.name
|
||||||
|
}
|
||||||
|
ContextCompat.startForegroundService(contextWrapper, intent)
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import ca.gosyer.jui.ui.library.settings.LibrarySettingsViewModel
|
|||||||
import ca.gosyer.jui.ui.main.MainViewModel
|
import ca.gosyer.jui.ui.main.MainViewModel
|
||||||
import ca.gosyer.jui.ui.main.about.AboutViewModel
|
import ca.gosyer.jui.ui.main.about.AboutViewModel
|
||||||
import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel
|
import ca.gosyer.jui.ui.main.components.DebugOverlayViewModel
|
||||||
|
import ca.gosyer.jui.ui.main.components.LibraryUpdatesViewModel
|
||||||
import ca.gosyer.jui.ui.manga.MangaScreenViewModel
|
import ca.gosyer.jui.ui.manga.MangaScreenViewModel
|
||||||
import ca.gosyer.jui.ui.reader.ReaderMenuViewModel
|
import ca.gosyer.jui.ui.reader.ReaderMenuViewModel
|
||||||
import ca.gosyer.jui.ui.settings.SettingsAdvancedViewModel
|
import ca.gosyer.jui.ui.settings.SettingsAdvancedViewModel
|
||||||
@@ -47,6 +48,7 @@ interface SharedViewModelComponent {
|
|||||||
val extensionsViewModel: () -> ExtensionsScreenViewModel
|
val extensionsViewModel: () -> ExtensionsScreenViewModel
|
||||||
val libraryViewModel: (SavedStateHandle) -> LibraryScreenViewModel
|
val libraryViewModel: (SavedStateHandle) -> LibraryScreenViewModel
|
||||||
val librarySettingsViewModel: () -> LibrarySettingsViewModel
|
val librarySettingsViewModel: () -> LibrarySettingsViewModel
|
||||||
|
val libraryUpdatesViewModel: (Boolean) -> LibraryUpdatesViewModel
|
||||||
val debugOverlayViewModel: () -> DebugOverlayViewModel
|
val debugOverlayViewModel: () -> DebugOverlayViewModel
|
||||||
val mainViewModel: () -> MainViewModel
|
val mainViewModel: () -> MainViewModel
|
||||||
val mangaViewModel: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel
|
val mangaViewModel: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class LibraryScreen : BaseScreen() {
|
|||||||
override fun Content() {
|
override fun Content() {
|
||||||
val vm = stateViewModel { libraryViewModel(it) }
|
val vm = stateViewModel { libraryViewModel(it) }
|
||||||
val settingsVM = viewModel { librarySettingsViewModel() }
|
val settingsVM = viewModel { librarySettingsViewModel() }
|
||||||
|
val updatesVM = viewModel { libraryUpdatesViewModel(false) }
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
LibraryScreenContent(
|
LibraryScreenContent(
|
||||||
categories = vm.categories.collectAsState().value,
|
categories = vm.categories.collectAsState().value,
|
||||||
@@ -40,6 +41,7 @@ class LibraryScreen : BaseScreen() {
|
|||||||
onPageChanged = vm::setSelectedPage,
|
onPageChanged = vm::setSelectedPage,
|
||||||
onClickManga = { navigator push MangaScreen(it) },
|
onClickManga = { navigator push MangaScreen(it) },
|
||||||
onRemoveMangaClicked = vm::removeManga,
|
onRemoveMangaClicked = vm::removeManga,
|
||||||
|
onUpdateLibrary = vm::updateLibrary,
|
||||||
showingMenu = vm.showingMenu.collectAsState().value,
|
showingMenu = vm.showingMenu.collectAsState().value,
|
||||||
setShowingMenu = vm::setShowingMenu,
|
setShowingMenu = vm::setShowingMenu,
|
||||||
libraryFilters = getLibraryFilters(settingsVM),
|
libraryFilters = getLibraryFilters(settingsVM),
|
||||||
@@ -48,7 +50,9 @@ class LibraryScreen : BaseScreen() {
|
|||||||
showUnread = vm.unreadBadges.collectAsState().value,
|
showUnread = vm.unreadBadges.collectAsState().value,
|
||||||
showDownloaded = vm.downloadBadges.collectAsState().value,
|
showDownloaded = vm.downloadBadges.collectAsState().value,
|
||||||
showLanguage = vm.languageBadges.collectAsState().value,
|
showLanguage = vm.languageBadges.collectAsState().value,
|
||||||
showLocal = vm.localBadges.collectAsState().value
|
showLocal = vm.localBadges.collectAsState().value,
|
||||||
|
updateWebsocketStatus = updatesVM.serviceStatus.collectAsState().value,
|
||||||
|
restartLibraryUpdates = updatesVM::restartLibraryUpdates
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.compose.material.ModalBottomSheetValue
|
|||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.FilterList
|
import androidx.compose.material.icons.rounded.FilterList
|
||||||
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
import androidx.compose.material.rememberModalBottomSheetState
|
import androidx.compose.material.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -37,11 +38,13 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ca.gosyer.jui.domain.base.WebsocketService
|
||||||
import ca.gosyer.jui.domain.category.model.Category
|
import ca.gosyer.jui.domain.category.model.Category
|
||||||
import ca.gosyer.jui.domain.library.model.DisplayMode
|
import ca.gosyer.jui.domain.library.model.DisplayMode
|
||||||
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.BackHandler
|
import ca.gosyer.jui.ui.base.navigation.BackHandler
|
||||||
|
import ca.gosyer.jui.ui.base.navigation.OverflowMode
|
||||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||||
import ca.gosyer.jui.ui.library.CategoryState
|
import ca.gosyer.jui.ui.library.CategoryState
|
||||||
import ca.gosyer.jui.ui.library.settings.LibrarySheet
|
import ca.gosyer.jui.ui.library.settings.LibrarySheet
|
||||||
@@ -70,6 +73,7 @@ fun LibraryScreenContent(
|
|||||||
onPageChanged: (Int) -> Unit,
|
onPageChanged: (Int) -> Unit,
|
||||||
onClickManga: (Long) -> Unit,
|
onClickManga: (Long) -> Unit,
|
||||||
onRemoveMangaClicked: (Long) -> Unit,
|
onRemoveMangaClicked: (Long) -> Unit,
|
||||||
|
onUpdateLibrary: () -> Unit,
|
||||||
showingMenu: Boolean,
|
showingMenu: Boolean,
|
||||||
setShowingMenu: (Boolean) -> Unit,
|
setShowingMenu: (Boolean) -> Unit,
|
||||||
libraryFilters: @Composable () -> Unit,
|
libraryFilters: @Composable () -> Unit,
|
||||||
@@ -78,7 +82,9 @@ fun LibraryScreenContent(
|
|||||||
showUnread: Boolean,
|
showUnread: Boolean,
|
||||||
showDownloaded: Boolean,
|
showDownloaded: Boolean,
|
||||||
showLanguage: Boolean,
|
showLanguage: Boolean,
|
||||||
showLocal: Boolean
|
showLocal: Boolean,
|
||||||
|
updateWebsocketStatus: WebsocketService.Status,
|
||||||
|
restartLibraryUpdates: () -> Unit
|
||||||
) {
|
) {
|
||||||
BackHandler(showingMenu) {
|
BackHandler(showingMenu) {
|
||||||
setShowingMenu(false)
|
setShowingMenu(false)
|
||||||
@@ -112,6 +118,7 @@ fun LibraryScreenContent(
|
|||||||
onPageChanged = onPageChanged,
|
onPageChanged = onPageChanged,
|
||||||
onClickManga = onClickManga,
|
onClickManga = onClickManga,
|
||||||
onRemoveMangaClicked = onRemoveMangaClicked,
|
onRemoveMangaClicked = onRemoveMangaClicked,
|
||||||
|
onUpdateLibrary = onUpdateLibrary,
|
||||||
showingMenu = showingMenu,
|
showingMenu = showingMenu,
|
||||||
setShowingMenu = setShowingMenu,
|
setShowingMenu = setShowingMenu,
|
||||||
libraryFilters = libraryFilters,
|
libraryFilters = libraryFilters,
|
||||||
@@ -138,6 +145,7 @@ fun LibraryScreenContent(
|
|||||||
onPageChanged = onPageChanged,
|
onPageChanged = onPageChanged,
|
||||||
onClickManga = onClickManga,
|
onClickManga = onClickManga,
|
||||||
onRemoveMangaClicked = onRemoveMangaClicked,
|
onRemoveMangaClicked = onRemoveMangaClicked,
|
||||||
|
onUpdateLibrary = onUpdateLibrary,
|
||||||
showingSheet = showingMenu,
|
showingSheet = showingMenu,
|
||||||
setShowingSheet = setShowingMenu,
|
setShowingSheet = setShowingMenu,
|
||||||
libraryFilters = libraryFilters,
|
libraryFilters = libraryFilters,
|
||||||
@@ -146,7 +154,9 @@ fun LibraryScreenContent(
|
|||||||
showUnread = showUnread,
|
showUnread = showUnread,
|
||||||
showDownloaded = showDownloaded,
|
showDownloaded = showDownloaded,
|
||||||
showLanguage = showLanguage,
|
showLanguage = showLanguage,
|
||||||
showLocal = showLocal
|
showLocal = showLocal,
|
||||||
|
updateWebsocketStatus = updateWebsocketStatus,
|
||||||
|
restartLibraryUpdates = restartLibraryUpdates
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,6 +178,7 @@ fun WideLibraryScreenContent(
|
|||||||
onPageChanged: (Int) -> Unit,
|
onPageChanged: (Int) -> Unit,
|
||||||
onClickManga: (Long) -> Unit,
|
onClickManga: (Long) -> Unit,
|
||||||
onRemoveMangaClicked: (Long) -> Unit,
|
onRemoveMangaClicked: (Long) -> Unit,
|
||||||
|
onUpdateLibrary: () -> Unit,
|
||||||
showingMenu: Boolean,
|
showingMenu: Boolean,
|
||||||
setShowingMenu: (Boolean) -> Unit,
|
setShowingMenu: (Boolean) -> Unit,
|
||||||
libraryFilters: @Composable () -> Unit,
|
libraryFilters: @Composable () -> Unit,
|
||||||
@@ -192,7 +203,8 @@ fun WideLibraryScreenContent(
|
|||||||
search = updateQuery,
|
search = updateQuery,
|
||||||
actions = {
|
actions = {
|
||||||
getActionItems(
|
getActionItems(
|
||||||
onToggleFiltersClick = { setShowingMenu(true) }
|
onToggleFiltersClick = { setShowingMenu(true) },
|
||||||
|
onUpdateLibrary = onUpdateLibrary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -269,6 +281,7 @@ fun ThinLibraryScreenContent(
|
|||||||
onPageChanged: (Int) -> Unit,
|
onPageChanged: (Int) -> Unit,
|
||||||
onClickManga: (Long) -> Unit,
|
onClickManga: (Long) -> Unit,
|
||||||
onRemoveMangaClicked: (Long) -> Unit,
|
onRemoveMangaClicked: (Long) -> Unit,
|
||||||
|
onUpdateLibrary: () -> Unit,
|
||||||
showingSheet: Boolean,
|
showingSheet: Boolean,
|
||||||
setShowingSheet: (Boolean) -> Unit,
|
setShowingSheet: (Boolean) -> Unit,
|
||||||
libraryFilters: @Composable () -> Unit,
|
libraryFilters: @Composable () -> Unit,
|
||||||
@@ -277,7 +290,9 @@ fun ThinLibraryScreenContent(
|
|||||||
showUnread: Boolean,
|
showUnread: Boolean,
|
||||||
showDownloaded: Boolean,
|
showDownloaded: Boolean,
|
||||||
showLanguage: Boolean,
|
showLanguage: Boolean,
|
||||||
showLocal: Boolean
|
showLocal: Boolean,
|
||||||
|
updateWebsocketStatus: WebsocketService.Status,
|
||||||
|
restartLibraryUpdates: () -> Unit
|
||||||
) {
|
) {
|
||||||
val bottomSheetState = rememberModalBottomSheetState(
|
val bottomSheetState = rememberModalBottomSheetState(
|
||||||
ModalBottomSheetValue.Hidden,
|
ModalBottomSheetValue.Hidden,
|
||||||
@@ -311,7 +326,10 @@ fun ThinLibraryScreenContent(
|
|||||||
search = updateQuery,
|
search = updateQuery,
|
||||||
actions = {
|
actions = {
|
||||||
getActionItems(
|
getActionItems(
|
||||||
onToggleFiltersClick = { setShowingSheet(true) }
|
onToggleFiltersClick = { setShowingSheet(true) },
|
||||||
|
onUpdateLibrary = onUpdateLibrary,
|
||||||
|
updateWebsocketStatus = updateWebsocketStatus,
|
||||||
|
restartLibraryUpdates = restartLibraryUpdates
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -361,13 +379,30 @@ fun ThinLibraryScreenContent(
|
|||||||
@Composable
|
@Composable
|
||||||
@Stable
|
@Stable
|
||||||
private fun getActionItems(
|
private fun getActionItems(
|
||||||
onToggleFiltersClick: () -> Unit
|
onToggleFiltersClick: () -> Unit,
|
||||||
|
onUpdateLibrary: () -> Unit,
|
||||||
|
updateWebsocketStatus: WebsocketService.Status? = null,
|
||||||
|
restartLibraryUpdates: (() -> Unit)? = null
|
||||||
): ImmutableList<ActionItem> {
|
): ImmutableList<ActionItem> {
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
ActionItem(
|
ActionItem(
|
||||||
name = stringResource(MR.strings.action_filter),
|
name = stringResource(MR.strings.action_filter),
|
||||||
icon = Icons.Rounded.FilterList,
|
icon = Icons.Rounded.FilterList,
|
||||||
doAction = onToggleFiltersClick
|
doAction = onToggleFiltersClick
|
||||||
)
|
),
|
||||||
|
ActionItem(
|
||||||
|
name = stringResource(MR.strings.action_update_library),
|
||||||
|
icon = Icons.Rounded.Refresh,
|
||||||
|
doAction = onUpdateLibrary
|
||||||
|
),
|
||||||
|
if (updateWebsocketStatus == WebsocketService.Status.STOPPED && restartLibraryUpdates != null) {
|
||||||
|
ActionItem(
|
||||||
|
name = stringResource(MR.strings.action_restart_library),
|
||||||
|
overflowMode = OverflowMode.ALWAYS_OVERFLOW,
|
||||||
|
doAction = restartLibraryUpdates
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
).toImmutableList()
|
).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import ca.gosyer.jui.ui.extensions.ExtensionsScreen
|
|||||||
import ca.gosyer.jui.ui.library.LibraryScreen
|
import ca.gosyer.jui.ui.library.LibraryScreen
|
||||||
import ca.gosyer.jui.ui.main.about.AboutScreen
|
import ca.gosyer.jui.ui.main.about.AboutScreen
|
||||||
import ca.gosyer.jui.ui.main.components.DownloadsExtraInfo
|
import ca.gosyer.jui.ui.main.components.DownloadsExtraInfo
|
||||||
|
import ca.gosyer.jui.ui.main.components.LibraryUpdatesExtraInfo
|
||||||
import ca.gosyer.jui.ui.main.more.MoreScreen
|
import ca.gosyer.jui.ui.main.more.MoreScreen
|
||||||
import ca.gosyer.jui.ui.settings.SettingsScreen
|
import ca.gosyer.jui.ui.settings.SettingsScreen
|
||||||
import ca.gosyer.jui.ui.sources.SourcesScreen
|
import ca.gosyer.jui.ui.sources.SourcesScreen
|
||||||
@@ -59,7 +60,7 @@ enum class TopLevelMenus(
|
|||||||
override val createScreen: () -> Screen,
|
override val createScreen: () -> Screen,
|
||||||
override val extraInfo: (@Composable () -> Unit)? = null
|
override val extraInfo: (@Composable () -> Unit)? = null
|
||||||
) : Menu {
|
) : Menu {
|
||||||
Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }),
|
Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }, extraInfo = { LibraryUpdatesExtraInfo() }),
|
||||||
Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }),
|
Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }),
|
||||||
Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }),
|
Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }),
|
||||||
Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }),
|
Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }),
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.ui.main.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
|
import androidx.compose.material.LocalContentColor
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.library.model.JobStatus
|
||||||
|
import ca.gosyer.jui.i18n.MR
|
||||||
|
import ca.gosyer.jui.ui.base.LocalViewModels
|
||||||
|
import ca.gosyer.jui.uicore.resources.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryUpdatesExtraInfo() {
|
||||||
|
val viewModels = LocalViewModels.current
|
||||||
|
val vm = remember { viewModels.libraryUpdatesViewModel(true) }
|
||||||
|
DisposableEffect(vm) {
|
||||||
|
onDispose(vm::onDispose)
|
||||||
|
}
|
||||||
|
val serviceStatus by vm.serviceStatus.collectAsState()
|
||||||
|
val updateStatus by vm.updateStatus.collectAsState()
|
||||||
|
|
||||||
|
fun Map<JobStatus, List<*>>.getSize(jobStatus: JobStatus): Int = get(jobStatus)?.size ?: 0
|
||||||
|
val current = remember(updateStatus) {
|
||||||
|
updateStatus.statusMap.run {
|
||||||
|
getSize(JobStatus.COMPLETE) + getSize(JobStatus.FAILED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val total = remember(updateStatus) {
|
||||||
|
updateStatus.statusMap.run {
|
||||||
|
getSize(JobStatus.COMPLETE) + getSize(JobStatus.FAILED) + getSize(JobStatus.PENDING) + getSize(JobStatus.RUNNING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val text = when (serviceStatus) {
|
||||||
|
WebsocketService.Status.STARTING -> stringResource(MR.strings.downloads_loading)
|
||||||
|
WebsocketService.Status.RUNNING -> {
|
||||||
|
if (updateStatus.running) {
|
||||||
|
stringResource(MR.strings.notification_updating, current, total)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WebsocketService.Status.STOPPED -> null
|
||||||
|
}
|
||||||
|
if (!text.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
|
||||||
|
)
|
||||||
|
} else if (serviceStatus == WebsocketService.Status.STOPPED) {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.clickable(onClick = vm::restartLibraryUpdates)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(MR.strings.downloads_stopped),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = Color.Red.copy(alpha = ContentAlpha.disabled)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.ui.main.components
|
||||||
|
|
||||||
|
import ca.gosyer.jui.domain.base.WebsocketService
|
||||||
|
import ca.gosyer.jui.domain.library.service.LibraryUpdateService
|
||||||
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
|
|
||||||
|
internal expect fun startLibraryUpdatesService(
|
||||||
|
contextWrapper: ContextWrapper,
|
||||||
|
libraryUpdatesService: LibraryUpdateService,
|
||||||
|
actions: WebsocketService.Actions
|
||||||
|
)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.ui.main.components
|
||||||
|
|
||||||
|
import ca.gosyer.jui.domain.base.WebsocketService.Actions
|
||||||
|
import ca.gosyer.jui.domain.library.service.LibraryUpdateService
|
||||||
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
|
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import me.tatarka.inject.annotations.Inject
|
||||||
|
import org.lighthousegames.logging.logging
|
||||||
|
|
||||||
|
class LibraryUpdatesViewModel @Inject constructor(
|
||||||
|
private val libraryUpdateService: LibraryUpdateService,
|
||||||
|
private val contextWrapper: ContextWrapper,
|
||||||
|
standalone: Boolean
|
||||||
|
) : ViewModel(contextWrapper) {
|
||||||
|
private val uiScope = if (standalone) {
|
||||||
|
MainScope()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
override val scope: CoroutineScope
|
||||||
|
get() = uiScope ?: super.scope
|
||||||
|
|
||||||
|
val serviceStatus = LibraryUpdateService.status.asStateFlow()
|
||||||
|
val updateStatus = LibraryUpdateService.updateStatus.asStateFlow()
|
||||||
|
|
||||||
|
fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART)
|
||||||
|
|
||||||
|
override fun onDispose() {
|
||||||
|
super.onDispose()
|
||||||
|
uiScope?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val log = logging()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.jui.ui.main.components
|
||||||
|
|
||||||
|
import ca.gosyer.jui.domain.base.WebsocketService
|
||||||
|
import ca.gosyer.jui.domain.library.service.LibraryUpdateService
|
||||||
|
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||||
|
|
||||||
|
internal actual fun startLibraryUpdatesService(
|
||||||
|
contextWrapper: ContextWrapper,
|
||||||
|
libraryUpdatesService: LibraryUpdateService,
|
||||||
|
actions: WebsocketService.Actions
|
||||||
|
) {
|
||||||
|
libraryUpdatesService.init()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user