Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SnackbarHostState?> { null }

object CloudStreamTheme {
val colors: CloudStreamColorScheme
Expand Down Expand Up @@ -50,7 +59,6 @@ fun CloudStreamTheme(
) {
val systemDark = isSystemInDarkTheme()
val dynamicTheme = resolveDynamicTheme()

val dynamicPrimary = resolveDynamicPrimaryColor()
val dynamicSecondary = resolveDynamicSecondaryColor()

Expand All @@ -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) },
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ToastEvent>(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)
}
Original file line number Diff line number Diff line change
@@ -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<ToastEvent>()
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(),
)
}