mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Support toasts on desktop, start working on toasting errors
This commit is contained in:
@@ -11,12 +11,16 @@ import ca.gosyer.jui.data.DataComponent
|
||||
import ca.gosyer.jui.domain.DomainComponent
|
||||
import ca.gosyer.jui.ui.ViewModelComponent
|
||||
import ca.gosyer.jui.ui.base.UiComponent
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import me.tatarka.inject.annotations.Component
|
||||
import me.tatarka.inject.annotations.Provides
|
||||
|
||||
@AppScope
|
||||
@Component
|
||||
abstract class AppComponent : ViewModelComponent, DataComponent, DomainComponent, UiComponent {
|
||||
abstract class AppComponent(
|
||||
@get:Provides
|
||||
val context: ContextWrapper
|
||||
) : ViewModelComponent, DataComponent, DomainComponent, UiComponent {
|
||||
|
||||
abstract val appMigrations: AppMigrations
|
||||
|
||||
@@ -31,7 +35,7 @@ abstract class AppComponent : ViewModelComponent, DataComponent, DomainComponent
|
||||
companion object {
|
||||
private var appComponentInstance: AppComponent? = null
|
||||
|
||||
fun getInstance() = appComponentInstance ?: create()
|
||||
fun getInstance(context: ContextWrapper) = appComponentInstance ?: create(context)
|
||||
.also { appComponentInstance = it }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,35 @@ package ca.gosyer.jui.desktop
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.configureSwingGlobalsForCompose
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toPainter
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.awaitApplication
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
@@ -44,6 +58,8 @@ import ca.gosyer.jui.ui.util.compose.WindowGet
|
||||
import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import ca.gosyer.jui.uicore.prefs.asStateIn
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.Length
|
||||
import com.github.weisj.darklaf.LafManager
|
||||
import com.github.weisj.darklaf.theme.DarculaTheme
|
||||
import com.github.weisj.darklaf.theme.IntelliJTheme
|
||||
@@ -53,6 +69,7 @@ import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -61,6 +78,8 @@ import org.jetbrains.skiko.SystemTheme
|
||||
import org.jetbrains.skiko.currentSystemTheme
|
||||
import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
import kotlin.time.Duration.Companion.ZERO
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun main() {
|
||||
@@ -70,7 +89,9 @@ suspend fun main() {
|
||||
System.setProperty("kotlinx.coroutines.debug", "on")
|
||||
}
|
||||
|
||||
val appComponent = AppComponent.getInstance()
|
||||
val context = ContextWrapper()
|
||||
|
||||
val appComponent = AppComponent.getInstance(context)
|
||||
appComponent.migrations.runMigrations()
|
||||
appComponent.appMigrations.runMigrations()
|
||||
|
||||
@@ -191,6 +212,12 @@ suspend fun main() {
|
||||
if (displayDebugInfo) {
|
||||
DebugOverlay()
|
||||
}
|
||||
ToastOverlay(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 64.dp),
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
ServerResult.STARTING, ServerResult.FAILED -> {
|
||||
@@ -224,3 +251,50 @@ suspend fun main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToastOverlay(modifier: Modifier, context: ContextWrapper) {
|
||||
var toast by remember { mutableStateOf<Pair<String, Length>?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
context.toasts
|
||||
.onEach {
|
||||
toast = it
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
LaunchedEffect(toast) {
|
||||
if (toast != null) {
|
||||
delay(
|
||||
when (toast?.second) {
|
||||
Length.SHORT -> 2.seconds
|
||||
Length.LONG -> 5.seconds
|
||||
else -> ZERO
|
||||
}
|
||||
)
|
||||
toast = null
|
||||
}
|
||||
}
|
||||
@Suppress("NAME_SHADOWING")
|
||||
Crossfade(
|
||||
toast?.first,
|
||||
modifier = modifier
|
||||
) { toast ->
|
||||
if (toast != null) {
|
||||
Card(
|
||||
Modifier.sizeIn(maxWidth = 200.dp),
|
||||
shape = CircleShape,
|
||||
backgroundColor = Color.DarkGray,
|
||||
) {
|
||||
Text(
|
||||
toast,
|
||||
Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
color = Color.White,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class GetMangaCategories @Inject constructor(private val categoryRepository: CategoryRepository) {
|
||||
|
||||
suspend fun await(mangaId: Long) = asFlow(mangaId)
|
||||
.catch { log.warn(it) { "Failed to get categories for $mangaId" } }
|
||||
suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories for $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(manga: Manga) = asFlow(manga)
|
||||
.catch { log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" } }
|
||||
suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId)
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class GetManga @Inject constructor(private val mangaRepository: MangaRepository) {
|
||||
|
||||
suspend fun await(mangaId: Long) = asFlow(mangaId)
|
||||
.catch { log.warn(it) { "Failed to get manga $mangaId" } }
|
||||
suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(manga: Manga) = asFlow(manga)
|
||||
.catch { log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" } }
|
||||
suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = mangaRepository.getManga(mangaId)
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class RefreshManga @Inject constructor(private val mangaRepository: MangaRepository) {
|
||||
|
||||
suspend fun await(mangaId: Long) = asFlow(mangaId)
|
||||
.catch { log.warn(it) { "Failed to refresh manga $mangaId" } }
|
||||
suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh manga $mangaId" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(manga: Manga) = asFlow(manga)
|
||||
.catch { log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" } }
|
||||
suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(mangaId: Long) = mangaRepository.getManga(mangaId, true)
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class GetLatestManga @Inject constructor(private val sourceRepository: SourceRepository) {
|
||||
|
||||
suspend fun await(source: Source, page: Int) = asFlow(source.id, page)
|
||||
.catch { log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" } }
|
||||
suspend fun await(source: Source, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(sourceId: Long, page: Int) = asFlow(sourceId, page)
|
||||
.catch { log.warn(it) { "Failed to get latest manga from $sourceId on page $page" } }
|
||||
suspend fun await(sourceId: Long, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get latest manga from $sourceId on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source, page: Int) = sourceRepository.getLatestManga(source.id, page)
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class GetPopularManga @Inject constructor(private val sourceRepository: SourceRepository) {
|
||||
|
||||
suspend fun await(source: Source, page: Int) = asFlow(source.id, page)
|
||||
.catch { log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" } }
|
||||
suspend fun await(source: Source, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(sourceId: Long, page: Int) = asFlow(sourceId, page)
|
||||
.catch { log.warn(it) { "Failed to get popular manga from $sourceId on page $page" } }
|
||||
suspend fun await(sourceId: Long, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get popular manga from $sourceId on page $page" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source, page: Int) = sourceRepository.getPopularManga(source.id, page)
|
||||
|
||||
@@ -15,12 +15,18 @@ import org.lighthousegames.logging.logging
|
||||
|
||||
class GetSearchManga @Inject constructor(private val sourceRepository: SourceRepository) {
|
||||
|
||||
suspend fun await(source: Source, searchTerm: String?, page: Int) = asFlow(source.id, searchTerm, page)
|
||||
.catch { log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" } }
|
||||
suspend fun await(source: Source, searchTerm: String?, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, searchTerm, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
suspend fun await(sourceId: Long, searchTerm: String?, page: Int) = asFlow(sourceId, searchTerm, page)
|
||||
.catch { log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" } }
|
||||
suspend fun await(sourceId: Long, searchTerm: String?, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, searchTerm, page)
|
||||
.catch {
|
||||
onError(it)
|
||||
log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" }
|
||||
}
|
||||
.singleOrNull()
|
||||
|
||||
fun asFlow(source: Source, searchTerm: String?, page: Int) = sourceRepository.getSearchResults(
|
||||
|
||||
@@ -151,21 +151,17 @@ class MangaScreenViewModel @Inject constructor(
|
||||
private suspend fun refreshMangaAsync(mangaId: Long, refresh: Boolean = false) = withIOContext {
|
||||
async {
|
||||
val manga = if (refresh) {
|
||||
refreshManga.await(mangaId)
|
||||
refreshManga.await(mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
} else {
|
||||
getManga.await(mangaId)
|
||||
getManga.await(mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
if (manga != null) {
|
||||
_manga.value = manga
|
||||
} else {
|
||||
// TODO: 2022-07-01 Error toast
|
||||
}
|
||||
|
||||
val mangaCategories = getMangaCategories.await(mangaId)
|
||||
val mangaCategories = getMangaCategories.await(mangaId, onError = { toast(it.message.orEmpty()) })
|
||||
if (mangaCategories != null) {
|
||||
_mangaCategories.value = mangaCategories.toImmutableList()
|
||||
} else {
|
||||
// TODO: 2022-07-01 Error toast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,13 +134,14 @@ class SourceScreenViewModel(
|
||||
|
||||
private suspend fun getPage(): MangaPage? {
|
||||
return when {
|
||||
isLatest.value -> getLatestManga.await(source, pageNum.value)
|
||||
isLatest.value -> getLatestManga.await(source, pageNum.value, onError = { toast(it.message.orEmpty()) })
|
||||
_query.value != null || _usingFilters.value -> getSearchManga.await(
|
||||
source.id,
|
||||
_query.value,
|
||||
pageNum.value
|
||||
sourceId = source.id,
|
||||
searchTerm = _query.value,
|
||||
page = pageNum.value,
|
||||
onError = { toast(it.message.orEmpty()) }
|
||||
)
|
||||
else -> getPopularManga.await(source.id, pageNum.value)
|
||||
else -> getPopularManga.await(source.id, pageNum.value, onError = { toast(it.message.orEmpty()) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ abstract class ViewModel(private val contextWrapper: ContextWrapper) : ScreenMod
|
||||
fun StringResource.toPlatformString(vararg args: Any): String {
|
||||
return contextWrapper.toPlatformString(this, *args)
|
||||
}
|
||||
fun toast(string: String, length: Length) {
|
||||
fun toast(string: String, length: Length = Length.SHORT) {
|
||||
scope.launchUI {
|
||||
contextWrapper.toast(string, length)
|
||||
}
|
||||
|
||||
@@ -6,11 +6,19 @@
|
||||
|
||||
package ca.gosyer.jui.uicore.vm
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import ca.gosyer.jui.core.lang.launchDefault
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.format
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
@Stable
|
||||
actual class ContextWrapper {
|
||||
private val _toasts = MutableSharedFlow<Pair<String, Length>>()
|
||||
val toasts = _toasts.asSharedFlow()
|
||||
|
||||
actual class ContextWrapper @Inject constructor() {
|
||||
actual fun toPlatformString(stringResource: StringResource): String {
|
||||
return stringResource.localized()
|
||||
}
|
||||
@@ -18,5 +26,8 @@ actual class ContextWrapper @Inject constructor() {
|
||||
return stringResource.format(*args).localized()
|
||||
}
|
||||
actual fun toast(string: String, length: Length) {
|
||||
GlobalScope.launchDefault {
|
||||
_toasts.emit(string to length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user