Skip to content

Commit

Permalink
Merge pull request #495 from DimensionDev/feature/inapp_notification
Browse files Browse the repository at this point in the history
add in app notification
  • Loading branch information
Tlaster authored Sep 27, 2024
2 parents e7dea6f + 5eb567f commit 285767b
Show file tree
Hide file tree
Showing 9 changed files with 491 additions and 253 deletions.
1 change: 0 additions & 1 deletion app/src/main/java/dev/dimension/flare/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class MainActivity : ComponentActivity() {
installSplashScreen().setKeepOnScreenCondition { keepSplashOnScreen }
enableEdgeToEdge()
super.onCreate(savedInstanceState)
android.webkit.WebView.setWebContentsDebuggingEnabled(true)
setContent {
AppContainer(
afterInit = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.dimension.flare.common

import androidx.annotation.StringRes
import dev.dimension.flare.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

internal sealed interface Notification {
data class Progress(
val progress: Int,
val total: Int,
) : Notification {
val percentage: Float
get() = progress.toFloat() / total
}

data class StringNotification(
@StringRes val messageId: Int,
val success: Boolean,
) : Notification
}

internal class ComposeInAppNotification : InAppNotification {
private val _source = MutableStateFlow(Event<Notification>(null, initialHandled = true))
val source
get() = _source.asStateFlow()

override fun onProgress(
message: Message,
progress: Int,
total: Int,
) {
_source.value = Event(Notification.Progress(progress, total))
}

override fun onSuccess(message: Message) {
val messageId =
when (message) {
Message.Compose -> R.string.compose_notification_success_title
}
_source.value = Event(Notification.StringNotification(messageId, success = true))
}

override fun onError(
message: Message,
throwable: Throwable,
) {
val messageId =
when (message) {
Message.Compose -> R.string.compose_notification_error_title
}
_source.value = Event(Notification.StringNotification(messageId, success = false))
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/dev/dimension/flare/common/Event.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.dimension.flare.common

class Event<out T>(
private val content: T?,
initialHandled: Boolean = false,
) {
@Suppress("MemberVisibilityCanBePrivate")
var hasBeenHandled = initialHandled
private set // Allow external read but not write

/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? =
if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/dev/dimension/flare/di/AndroidModule.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package dev.dimension.flare.di

import androidx.media3.common.util.UnstableApi
import dev.dimension.flare.common.ComposeInAppNotification
import dev.dimension.flare.common.InAppNotification
import dev.dimension.flare.common.PlayerPoll
import dev.dimension.flare.data.repository.ComposeNotifyUseCase
import dev.dimension.flare.data.repository.SettingsRepository
import dev.dimension.flare.ui.component.VideoPlayerPool
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.binds
import org.koin.dsl.module

@UnstableApi
Expand All @@ -15,4 +18,5 @@ val androidModule =
singleOf(::SettingsRepository)
singleOf(::PlayerPoll)
singleOf(::VideoPlayerPool)
singleOf(::ComposeInAppNotification) binds arrayOf(InAppNotification::class, ComposeInAppNotification::class)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package dev.dimension.flare.ui.component

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.res.stringResource
import androidx.compose.ui.unit.dp
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Solid
import compose.icons.fontawesomeicons.solid.CircleCheck
import compose.icons.fontawesomeicons.solid.CircleExclamation
import dev.dimension.flare.common.ComposeInAppNotification
import dev.dimension.flare.common.Notification
import kotlinx.coroutines.delay
import org.koin.compose.koinInject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

@Composable
internal fun InAppNotificationComponent(
modifier: Modifier = Modifier,
notification: ComposeInAppNotification = koinInject(),
) {
val source by notification.source.collectAsState()
val content = remember(source) { source.getContentIfNotHandled() }

content?.let {
when (it) {
is Notification.Progress -> {
LinearProgressIndicator(
progress = { it.percentage },
modifier =
modifier
.fillMaxWidth(),
)
}
is Notification.StringNotification -> {
var showNotification by remember { mutableStateOf(false) }
var showText by remember { mutableStateOf(false) }
LaunchedEffect(source) {
showNotification = true
delay(500.milliseconds)
showText = true
delay(3.seconds)
showNotification = false
}
AnimatedVisibility(
showNotification,
modifier =
modifier
.systemBarsPadding(),
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
) {
Surface(
shape = MaterialTheme.shapes.large,
shadowElevation = 8.dp,
tonalElevation = 8.dp,
) {
Row(
modifier =
Modifier
.padding(12.dp)
.animateContentSize(),
verticalAlignment = Alignment.CenterVertically,
) {
if (it.success) {
FAIcon(
FontAwesomeIcons.Solid.CircleCheck,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
} else {
FAIcon(
FontAwesomeIcons.Solid.CircleExclamation,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
}
AnimatedVisibility(showText) {
Row {
Spacer(modifier = Modifier.width(12.dp))
Text(
stringResource(it.messageId),
)
}
}
}
}
}
}
}
}
}
Loading

0 comments on commit 285767b

Please sign in to comment.