From c5e0cf119e1f8629eaef80ec5abf227891d31d85 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 4 Nov 2022 23:50:24 -0400 Subject: [PATCH] Support toasts on desktop, start working on toasting errors --- .../ca/gosyer/jui/desktop/AppComponent.kt | 8 +- .../main/kotlin/ca/gosyer/jui/desktop/main.kt | 76 ++++++++++++++++++- .../category/interactor/GetMangaCategories.kt | 14 +++- .../jui/domain/manga/interactor/GetManga.kt | 14 +++- .../domain/manga/interactor/RefreshManga.kt | 14 +++- .../source/interactor/GetLatestManga.kt | 14 +++- .../source/interactor/GetPopularManga.kt | 14 +++- .../source/interactor/GetSearchManga.kt | 14 +++- .../jui/ui/manga/MangaScreenViewModel.kt | 10 +-- .../sources/browse/SourceScreenViewModel.kt | 11 +-- .../ca/gosyer/jui/uicore/vm/ViewModel.kt | 2 +- .../ca/gosyer/jui/uicore/vm/ContextWrapper.kt | 15 +++- 12 files changed, 164 insertions(+), 42 deletions(-) diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt index fd862a64..985352c5 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt @@ -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 } } } diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt index 6ca46561..7256e445 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt @@ -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?>(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 + ) + } + } + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt index 11d60ca6..3f8f0aeb 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt @@ -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) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt index 014384f1..dad8562d 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt @@ -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) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt index eecd06ce..33d23bdc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt @@ -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) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt index fa8f86b9..f7c5667c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt @@ -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) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt index 2e1ceded..2afecf22 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt @@ -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) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt index 461e49e8..ce254a28 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt @@ -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( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt index 8216bd21..64b4a608 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt @@ -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 } } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt index 87974bca..3640a809 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt @@ -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()) }) } } diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt index 00c6259f..c42e2d91 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt @@ -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) } diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt index b38e4fd5..094705f7 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt @@ -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>() + 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) + } } }