diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 675ce3b2f77..bf499d2a77e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,7 @@ jobs: cache-read-only: false - name: Run Gradle - run: ./gradlew assemblePrereleaseDebug lint + run: ./gradlew assemblePrereleaseDebug - name: Upload Artifact uses: actions/upload-artifact@v7 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt index 5b1f1d1825a..cccd28ec467 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/compose/settings/SettingsFragment.kt @@ -64,7 +64,11 @@ class SettingsFragment : Fragment() { val v = version.appVersion val h = version.commitHash val d = version.buildDate - clipboardHelper(txt(R.string.extension_version), "$v $h $d") + // clipboardHelper(txt(R.string.extension_version), "$v $h $d") + com.lagradost.cloudstream4.compose.toast.ToastController.post("Test default") + com.lagradost.cloudstream4.compose.toast.ToastController.postSuccess("Test success") + com.lagradost.cloudstream4.compose.toast.ToastController.postError("Test error") + com.lagradost.cloudstream4.compose.toast.ToastController.postWarning("Test warning") }, ) } diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt index 03cee9b256f..b3ca84f18d0 100644 --- a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/theme/CloudStreamTheme.kt @@ -1,13 +1,22 @@ package com.lagradost.cloudstream4.compose.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import com.lagradost.cloudstream4.compose.toast.CloudStreamSnackbar +import com.lagradost.cloudstream4.compose.toast.ToastEffectHost val LocalCloudStreamColors = staticCompositionLocalOf { darkScheme() } +val LocalSnackbarHostState = compositionLocalOf { null } object CloudStreamTheme { val colors: CloudStreamColorScheme @@ -50,7 +59,6 @@ fun CloudStreamTheme( ) { val systemDark = isSystemInDarkTheme() val dynamicTheme = resolveDynamicTheme() - val dynamicPrimary = resolveDynamicPrimaryColor() val dynamicSecondary = resolveDynamicSecondaryColor() @@ -73,10 +81,27 @@ fun CloudStreamTheme( } } - CompositionLocalProvider(LocalCloudStreamColors provides csColors) { - MaterialTheme( - colorScheme = csColors.toMaterial3ColorScheme(), - content = content, - ) + val parentHostState = LocalSnackbarHostState.current + val isRoot = parentHostState == null + val hostState = remember { parentHostState ?: SnackbarHostState() } + + if (isRoot) ToastEffectHost(hostState) + + CompositionLocalProvider( + LocalCloudStreamColors provides csColors, + LocalSnackbarHostState provides hostState, + ) { + MaterialTheme(colorScheme = csColors.toMaterial3ColorScheme()) { + Box(modifier = Modifier.fillMaxSize()) { + content() + if (isRoot) { + SnackbarHost( + hostState = hostState, + modifier = Modifier.align(Alignment.BottomCenter), + snackbar = { CloudStreamSnackbar(it) }, + ) + } + } + } } } diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/toast/ToastController.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/toast/ToastController.kt new file mode 100644 index 00000000000..05a1f331ce9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/toast/ToastController.kt @@ -0,0 +1,92 @@ +package com.lagradost.cloudstream4.compose.toast + +import androidx.compose.material3.SnackbarDuration +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +enum class ToastType { Info, Success, Warning, Error } + +data class ToastEvent( + val message: String, + val type: ToastType = ToastType.Info, + val duration: SnackbarDuration = SnackbarDuration.Short, + val actionLabel: String? = null, + val onAction: (() -> Unit)? = null, + val dismissable: Boolean = false, + val queue: Boolean = false, +) + +object ToastController { + private val _events = Channel(Channel.BUFFERED) + internal val events = _events.receiveAsFlow() + + internal fun drain(): ToastEvent? = _events.tryReceive().getOrNull() + + fun post( + message: String, + type: ToastType = ToastType.Info, + duration: SnackbarDuration = SnackbarDuration.Short, + actionLabel: String? = null, + onAction: (() -> Unit)? = null, + dismissable: Boolean = false, + queue: Boolean = false, + ) { + if (!queue) _events.tryReceive() + _events.trySend(ToastEvent(message, type, duration, actionLabel, onAction, dismissable, queue)) + } + + fun postSuccess( + message: String, + duration: SnackbarDuration = SnackbarDuration.Short, + dismissable: Boolean = true, + queue: Boolean = true, + ) = post(message, ToastType.Success, duration, dismissable = dismissable, queue = queue) + + fun postWarning( + message: String, + duration: SnackbarDuration = SnackbarDuration.Short, + dismissable: Boolean = true, + queue: Boolean = true, + ) = post(message, ToastType.Warning, duration, dismissable = dismissable, queue = queue) + + fun postError( + message: String, + duration: SnackbarDuration = SnackbarDuration.Long, + dismissable: Boolean = true, + queue: Boolean = true, + ) = post(message, ToastType.Error, duration, dismissable = dismissable, queue = queue) + + suspend fun show( + message: String, + type: ToastType = ToastType.Info, + duration: SnackbarDuration = SnackbarDuration.Short, + actionLabel: String? = null, + onAction: (() -> Unit)? = null, + dismissable: Boolean = false, + queue: Boolean = false, + ) { + if (!queue) _events.tryReceive() + _events.send(ToastEvent(message, type, duration, actionLabel, onAction, dismissable, queue)) + } + + suspend fun showSuccess( + message: String, + duration: SnackbarDuration = SnackbarDuration.Short, + dismissable: Boolean = true, + queue: Boolean = true, + ) = show(message, ToastType.Success, duration, dismissable = dismissable, queue = queue) + + suspend fun showWarning( + message: String, + duration: SnackbarDuration = SnackbarDuration.Short, + dismissable: Boolean = true, + queue: Boolean = true, + ) = show(message, ToastType.Warning, duration, dismissable = dismissable, queue = queue) + + suspend fun showError( + message: String, + duration: SnackbarDuration = SnackbarDuration.Long, + dismissable: Boolean = true, + queue: Boolean = true, + ) = show(message, ToastType.Error, duration, dismissable = dismissable, queue = queue) +} diff --git a/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/toast/ToastHost.kt b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/toast/ToastHost.kt new file mode 100644 index 00000000000..a96a642aa1a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/lagradost/cloudstream4/compose/toast/ToastHost.kt @@ -0,0 +1,89 @@ +package com.lagradost.cloudstream4.compose.toast + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.lagradost.cloudstream4.compose.theme.CloudStreamTheme + +internal class ToastVisuals(val event: ToastEvent) : SnackbarVisuals { + override val message: String = event.message + override val actionLabel: String? = event.actionLabel + override val duration: SnackbarDuration = event.duration + override val withDismissAction: Boolean = event.dismissable +} + +@Composable +internal fun ToastEffectHost(hostState: SnackbarHostState) { + LaunchedEffect(hostState) { + var showingMessage: String? = null + ToastController.events.collect { event -> + if (!event.queue) { + hostState.currentSnackbarData?.dismiss() + var latest = event + val queued = mutableListOf() + var next = ToastController.drain() + while (next != null) { + if (next.queue) queued.add(next) + else latest = next + next = ToastController.drain() + } + if (latest.message != showingMessage) { + showingMessage = latest.message + val result = hostState.showSnackbar(ToastVisuals(latest)) + if (result == SnackbarResult.ActionPerformed) latest.onAction?.invoke() + var leftover = ToastController.drain() + while (leftover != null) { + if (leftover.queue) queued.add(leftover) + leftover = ToastController.drain() + } + showingMessage = null + } + for (q in queued) { + val result = hostState.showSnackbar(ToastVisuals(q)) + if (result == SnackbarResult.ActionPerformed) q.onAction?.invoke() + } + } else { + val result = hostState.showSnackbar(ToastVisuals(event)) + if (result == SnackbarResult.ActionPerformed) event.onAction?.invoke() + } + } + } +} + +@Composable +private fun ToastType.containerColor(): Color { + val c = CloudStreamTheme.colors + return when (this) { + ToastType.Info -> c.background + ToastType.Success -> c.primary.copy(alpha = 0.90f) + ToastType.Warning -> Color(0xFFB45309) + ToastType.Error -> Color(0xFFB91C1C) + } +} + +@Composable +private fun ToastType.contentColor(): Color = when (this) { + ToastType.Info -> CloudStreamTheme.colors.onBackground + else -> Color.White +} + +@Composable +fun CloudStreamSnackbar(data: SnackbarData) { + val type = (data.visuals as? ToastVisuals)?.event?.type ?: ToastType.Info + Snackbar( + snackbarData = data, + shape = RoundedCornerShape(12.dp), + containerColor = type.containerColor(), + contentColor = type.contentColor(), + actionColor = type.contentColor(), + dismissActionContentColor = type.contentColor(), + ) +}