diff --git a/build.gradle.kts b/build.gradle.kts index 12fa7e2..7879c11 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,7 @@ buildscript { } dependencies { classpath(libs.moko.resources.generator) + classpath(libs.atomicfu) } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt index ed8e498..3edc3c9 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt @@ -35,14 +35,14 @@ fun Component.onRender(content: @Composable () -> Unit) { } @Composable -fun Component.onRenderWithScheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { +fun Component.onRenderWithScheme(key: Any?, content: @Composable (SchemeTheme.Updater?) -> Unit) { onRender { SchemeTheme(key, content) } } @Composable -fun Component.onRenderApplyCommonScheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { +fun Component.onRenderApplyCommonScheme(key: Any?, content: @Composable (SchemeTheme.Updater?) -> Unit) { onRenderWithScheme(key, content) SchemeTheme.setCommon(key) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt index 0c7aa6a..0a75cf0 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendCompose.kt @@ -1,6 +1,5 @@ package dev.datlag.aniflow.common -import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding @@ -8,21 +7,19 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.painter.BrushPainter import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max +import com.kmpalette.DominantColorState import dev.datlag.aniflow.LocalPaddingValues fun Modifier.bottomShadowBrush(color: Color, alpha: Float = 1F): Modifier { @@ -108,44 +105,6 @@ fun Modifier.mergedLocalPadding(other: PaddingValues, additional: Dp) = composed this.mergedLocalPadding(other, PaddingValues(additional)) } -@Composable -private fun shimmerBrush(): Brush { - val shimmerColors = listOf( - Color.LightGray.copy(alpha = 0.2f), - Color.LightGray.copy(alpha = 0.3f), - Color.LightGray.copy(alpha = 0.4f), - Color.LightGray.copy(alpha = 0.3f), - Color.LightGray.copy(alpha = 0.2f), - ) - - val transition = rememberInfiniteTransition() - val translateAnimation by transition.animateFloat( - initialValue = 0f, - targetValue = 1500.toFloat(), - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1000, - easing = LinearEasing, - ), - repeatMode = RepeatMode.Restart, - ) - ) - - return Brush.linearGradient( - colors = shimmerColors, - start = Offset(x = translateAnimation - 500, y = 0.0f), - end = Offset(x = translateAnimation, y = 270F), - ) -} - -fun Modifier.shimmer(shape: Shape = RectangleShape): Modifier = composed { - this.background( - brush = shimmerBrush(), - shape = shape - ) -} - - @Composable fun LazyListState.isScrollingUp(): Boolean { var previousIndex by remember(this) { @@ -186,4 +145,12 @@ fun SheetState.isFullyExpandedOrTargeted(forceFullExpand: Boolean = false): Bool } return this.targetValue == checkState -} \ No newline at end of file +} + +val DominantColorState?.primary + @Composable + get() = this?.color ?: MaterialTheme.colorScheme.primary + +val DominantColorState?.onPrimary + @Composable + get() = this?.onColor ?: MaterialTheme.colorScheme.onPrimary \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt index 86c2cea..4e1724f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt @@ -22,6 +22,7 @@ import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.aniflow.ui.theme.SchemeTheme +import dev.datlag.aniflow.ui.theme.rememberSchemeThemeDominantColorState import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch @@ -36,14 +37,14 @@ fun AiringCard( onClick: (Medium) -> Unit ) { airing.media?.let(::Medium)?.let { media -> + val updater = SchemeTheme.create(media.id) + Card( modifier = modifier, onClick = { onClick(media) } ) { - val scope = rememberCoroutineScope() - Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -62,15 +63,15 @@ fun AiringCard( model = media.coverImage.medium, contentScale = ContentScale.Crop, onSuccess = { state -> - + updater?.update(state.painter) } ), onSuccess = { state -> - + updater?.update(state.painter) } ), onSuccess = { state -> - + updater?.update(state.painter) } ) Column( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt index d32d651..3370b68 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringOverview.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.AiringQuery import dev.datlag.aniflow.anilist.AiringTodayStateMachine import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.common.shimmer import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt index bee8fd4..b3f1347 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt @@ -1,10 +1,6 @@ package dev.datlag.aniflow.ui.navigation.screen.initial.home.component -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -12,10 +8,7 @@ import androidx.compose.material3.contentColorFor import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -23,15 +16,14 @@ import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.bottomShadowBrush +import dev.datlag.aniflow.common.onPrimary +import dev.datlag.aniflow.common.primary import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.settings.model.AppSettings -import dev.datlag.aniflow.ui.custom.alignment.rememberParallaxAlignment import dev.datlag.aniflow.ui.theme.SchemeTheme import dev.datlag.aniflow.ui.theme.rememberSchemeThemeDominantColorState import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch @OptIn(ExperimentalStdlibApi::class) @Composable @@ -43,7 +35,7 @@ fun MediumCard( ) { SchemeTheme( key = medium.id - ) { schemeState -> + ) { updater -> Card( modifier = modifier, onClick = { @@ -64,7 +56,6 @@ fun MediumCard( defaultColor = color ?: MaterialTheme.colorScheme.primary, defaultOnColor = contentColorFor(color ?: MaterialTheme.colorScheme.primary) ) - val scope = rememberCoroutineScope() AsyncImage( model = medium.coverImage.extraLarge, @@ -78,28 +69,18 @@ fun MediumCard( model = medium.coverImage.medium, contentScale = ContentScale.Crop, onSuccess = { state -> - scope.launch { - schemeState.updateFrom(state.painter) - } + updater?.update(state.painter) }, onError = { - color?.let(::ColorPainter)?.let { painter -> - scope.launch { - schemeState.updateFrom(painter) - } - } + updater?.update(color) } ), onSuccess = { state -> - scope.launch { - schemeState.updateFrom(state.painter) - } + updater?.update(state.painter) } ), onSuccess = { state -> - scope.launch { - schemeState.updateFrom(state.painter) - } + updater?.update(state.painter) } ) @@ -114,7 +95,7 @@ fun MediumCard( modifier = Modifier .align(Alignment.BottomStart) .fillMaxWidth() - .bottomShadowBrush(colorState.color) + .bottomShadowBrush(colorState.primary) .padding(16.dp) .padding(top = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) @@ -131,10 +112,10 @@ fun MediumCard( }, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxWidth(), - color = colorState.onColor + color = colorState.onPrimary ) if (medium.averageScore > 0F) { - Rating(medium, colorState.onColor) + Rating(medium, colorState.onPrimary) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt index a7aea65..f9310f5 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/PopularSeasonOverview.kt @@ -18,7 +18,6 @@ import dev.datlag.aniflow.anilist.SeasonQuery import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.SeasonState -import dev.datlag.aniflow.common.shimmer import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt index 76eed57..b9d2875 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/TrendingOverview.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.unit.dp import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine import dev.datlag.aniflow.anilist.TrendingQuery import dev.datlag.aniflow.anilist.model.Medium -import dev.datlag.aniflow.common.shimmer import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.settings.model.AppSettings import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt index 58703b1..25cc6c6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt @@ -1,6 +1,5 @@ package dev.datlag.aniflow.ui.theme -import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color @@ -10,38 +9,96 @@ import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter import com.kmpalette.DominantColorState import com.kmpalette.palette.graphics.Palette -import com.kmpalette.rememberDominantColorState import com.kmpalette.rememberPainterDominantColorState -import com.materialkolor.AnimatedDynamicMaterialTheme import com.materialkolor.DynamicMaterialTheme -import com.materialkolor.ktx.isDisliked import com.mayakapps.kache.InMemoryKache import com.mayakapps.kache.KacheStrategy import dev.datlag.aniflow.LocalDarkMode +import dev.datlag.aniflow.model.coroutines.Executor import dev.datlag.tooling.async.scopeCatching import dev.datlag.tooling.async.suspendCatching import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.compose.launchIO import dev.datlag.tooling.compose.withIOContext import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle -import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlin.coroutines.CoroutineContext data object SchemeTheme { + internal val executor = Executor() internal val commonSchemeKey = MutableStateFlow(null) - internal val kache = InMemoryKache>( + private val kache = InMemoryKache>( maxSize = 25L * 1024 * 1024 ) { strategy = KacheStrategy.LRU } + internal fun get(key: Any) = scopeCatching { + kache.getIfAvailable(key) + }.getOrNull() + + internal suspend fun getOrPut(key: Any, fallback: DominantColorState) = suspendCatching { + kache.getOrPut(key) { fallback } + }.getOrNull() + fun setCommon(key: Any?) { commonSchemeKey.update { key } } + + @Composable + fun create(key: Any?): Updater? { + if (key == null) { + return null + } + + val state = rememberSchemeThemeDominantColorState(key) + val scope = rememberCoroutineScope() + return remember(state, scope) { + state?.let { Updater.State(scope, it) } + } + } + + sealed interface Updater { + fun update(input: Painter?) + fun update(color: Color?) = update(color?.let(::ColorPainter)) + + data class State( + private val scope: CoroutineScope, + private val state: DominantColorState + ) : Updater { + override fun update(input: Painter?) { + if (input == null) { + return + } + + scope.launchIO { + executor.enqueue { + state.updateFrom(input) + } + } + } + } + + data class Default( + private val key: Any, + private val scope: CoroutineScope, + ) : Updater { + override fun update(input: Painter?) { + if (input == null) { + return + } + + scope.launchIO { + executor.enqueue { + val state = get(key) ?: return@enqueue + state.updateFrom(input) + } + } + } + } + } } @Composable @@ -52,10 +109,13 @@ fun rememberSchemeThemeDominantColorState( coroutineContext: CoroutineContext = ioDispatcher(), isSwatchValid: (Palette.Swatch) -> Boolean = { true }, builder: Palette.Builder.() -> Unit = {}, -): DominantColorState { +): DominantColorState? { + if (key == null) { + return null + } val fallbackState = remember(key) { - key?.let { SchemeTheme.kache.getIfAvailable(it) } + SchemeTheme.get(key) } ?: rememberPainterDominantColorState( defaultColor = defaultColor, defaultOnColor = defaultOnColor, @@ -65,9 +125,7 @@ fun rememberSchemeThemeDominantColorState( ) val state by produceState(fallbackState, key) { value = withIOContext { - key?.let { - SchemeTheme.kache.getOrPut(it) { fallbackState } - } ?: fallbackState + SchemeTheme.getOrPut(key, fallbackState) ?: fallbackState } } @@ -83,7 +141,7 @@ fun rememberSchemeThemeDominantColorState( applyMinContrast: Boolean = false, minContrastBackgroundColor: Color = Color.Transparent, coroutineContext: CoroutineContext = ioDispatcher() -): DominantColorState { +): DominantColorState? { return rememberSchemeThemeDominantColorState( key = key, defaultColor = defaultColor, @@ -107,20 +165,22 @@ fun rememberSchemeThemeDominantColorState( } @Composable -fun SchemeTheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { +fun SchemeTheme(key: Any?, content: @Composable (SchemeTheme.Updater?) -> Unit) { val state = rememberSchemeThemeDominantColorState(key) + val scope = rememberCoroutineScope() + val updater = remember(key, scope) { + key?.let { SchemeTheme.Updater.Default(it, scope) } + } ?: SchemeTheme.create(key) DynamicMaterialTheme( - seedColor = state.color, - useDarkTheme = LocalDarkMode.current, - animate = true + seedColor = state?.color ) { - content(state) + content(updater) } } @Composable -fun CommonSchemeTheme(content: @Composable (DominantColorState) -> Unit) { +fun CommonSchemeTheme(content: @Composable (SchemeTheme.Updater?) -> Unit) { val key by SchemeTheme.commonSchemeKey.collectAsStateWithLifecycle() SchemeTheme(key, content) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b548106..32b08fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ android-core = "1.13.0" android-credentials = "1.3.0-alpha02" apollo = "4.0.0-beta.6" appcompat = "1.6.1" +atomicfu = "0.24.0" coil = "3.0.0-alpha06" compose = "1.6.2" complete-kotlin = "1.1.0" @@ -56,6 +57,7 @@ activity = { group = "androidx.activity", name = "activity-ktx", version.ref = " activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } apollo = { group = "com.apollographql.apollo3", name = "apollo-runtime", version.ref = "apollo" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu-gradle-plugin", version.ref = "atomicfu" } coil = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" } coil-network = { group = "io.coil-kt.coil3", name = "coil-network-ktor", version.ref = "coil" } coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } diff --git a/model/build.gradle.kts b/model/build.gradle.kts index 095f795..7f688e5 100644 --- a/model/build.gradle.kts +++ b/model/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.multiplatform) + id("kotlinx-atomicfu") } kotlin { diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/ConflatedExecutor.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/ConflatedExecutor.kt new file mode 100644 index 0000000..e872cda --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/ConflatedExecutor.kt @@ -0,0 +1,35 @@ +package dev.datlag.aniflow.model.coroutines + +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.* + +interface ConflatedExecutor { + suspend fun conflate(block: suspend () -> T): T +} + +internal class ConflatedExecutorImpl : ConflatedExecutor { + private val activeTask: AtomicRef = atomic(null) + + suspend override fun conflate(block: suspend () -> T): T { + return coroutineScope { + val newTask = async(start = CoroutineStart.LAZY) { block() }.also { task -> + task.invokeOnCompletion { + activeTask.compareAndSet(task, null) + } + } + + val result: T + while (true) { + if (!activeTask.compareAndSet(null, newTask)) { + activeTask.value?.cancelAndJoin() + yield() + } else { + result = newTask.await() + break + } + } + return@coroutineScope result + } + } +} \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/Executor.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/Executor.kt new file mode 100644 index 0000000..fc110b0 --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/Executor.kt @@ -0,0 +1,24 @@ +package dev.datlag.aniflow.model.coroutines + +import kotlin.jvm.JvmOverloads + +class Executor @JvmOverloads constructor( + private val conflatedExecutor: ConflatedExecutor = ConflatedExecutorImpl(), + private val queueExecutor: QueueExecutor = QueueExecutorImpl() +) : ConflatedExecutor by conflatedExecutor, QueueExecutor by queueExecutor { + + suspend fun execute( + schema: Schema, + block: suspend () -> T + ): T { + return when (schema) { + is Schema.Conflated -> conflate(block) + is Schema.Queue -> enqueue(block) + } + } + + sealed interface Schema { + data object Conflated : Schema + data object Queue: Schema + } +} \ No newline at end of file diff --git a/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/QueueExecutor.kt b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/QueueExecutor.kt new file mode 100644 index 0000000..05b9dcc --- /dev/null +++ b/model/src/commonMain/kotlin/dev/datlag/aniflow/model/coroutines/QueueExecutor.kt @@ -0,0 +1,18 @@ +package dev.datlag.aniflow.model.coroutines + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +interface QueueExecutor { + suspend fun enqueue(block: suspend () -> T): T +} + +internal class QueueExecutorImpl : QueueExecutor { + private val mutex = Mutex() + + override suspend fun enqueue(block: suspend () -> T): T { + mutex.withLock { + return block() + } + } +} \ No newline at end of file