Feature/system notification (#101)

* feature: System notification

* feature: Type

* fix: Settings

---------

Co-authored-by: TEYSSANDIER Raphael <rteyssandier@sephora.fr>
This commit is contained in:
Raphael Teyssandier 2025-08-18 16:01:03 +02:00 committed by GitHub
parent 8147d8852c
commit 0fe9d4e60b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 158 additions and 51 deletions

View file

@ -1,8 +1,8 @@
package io.github.openflocon.flocondesktop.app.di
import io.github.openflocon.flocondesktop.app.InitialSetupStateHolder
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayerHandler
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayerHandler
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayerImpl
import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.singleOf

View file

@ -1,13 +0,0 @@
package io.github.openflocon.flocondesktop.common.ui.feedback
interface FeedbackDisplayer {
fun displayMessage(
message: String,
type: MessageType = MessageType.Success,
)
enum class MessageType {
Success,
Error,
}
}

View file

@ -1,13 +0,0 @@
package io.github.openflocon.flocondesktop.common.ui.feedback
import kotlinx.coroutines.flow.Flow
interface FeedbackDisplayerHandler {
val messagesToDisplay: Flow<MessageToDisplayUi>
data class MessageToDisplayUi(
val message: String,
val type: FeedbackDisplayer.MessageType,
val id: String,
)
}

View file

@ -1,6 +1,10 @@
package io.github.openflocon.flocondesktop.common.ui.feedback
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayerHandler
import io.github.openflocon.domain.feedback.FeedbackDisplayerHandler.MessageToDisplayUi
import io.github.openflocon.domain.feedback.FeedbackDisplayerHandler.NotificationToDisplayUi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.Channel
@ -14,10 +18,11 @@ class FeedbackDisplayerImpl(
FeedbackDisplayerHandler {
private val scope = CoroutineScope(dispatcherProvider.ui + SupervisorJob())
private val _messagesToDisplay: Channel<FeedbackDisplayerHandler.MessageToDisplayUi> =
Channel()
override val messagesToDisplay: Flow<FeedbackDisplayerHandler.MessageToDisplayUi> =
_messagesToDisplay.receiveAsFlow()
private val _messagesToDisplay: Channel<MessageToDisplayUi> = Channel()
override val messagesToDisplay: Flow<MessageToDisplayUi> = _messagesToDisplay.receiveAsFlow()
private val _notificationsToDisplay: Channel<NotificationToDisplayUi> = Channel()
override val notificationsToDisplay: Flow<NotificationToDisplayUi> = _notificationsToDisplay.receiveAsFlow()
override fun displayMessage(
message: String,
@ -25,7 +30,7 @@ class FeedbackDisplayerImpl(
) {
scope.launch {
_messagesToDisplay.send(
FeedbackDisplayerHandler.MessageToDisplayUi(
MessageToDisplayUi(
message = message,
type = type,
id = System.currentTimeMillis().toString(),
@ -33,4 +38,17 @@ class FeedbackDisplayerImpl(
)
}
}
override fun displayNotification(title: String, message: String, type: FeedbackDisplayer.NotificationType) {
scope.launch {
_notificationsToDisplay.send(
NotificationToDisplayUi(
title = title,
message = message,
type = type
)
)
}
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.openflocon.domain.feedback.FeedbackDisplayerHandler
import io.github.openflocon.library.designsystem.FloconTheme
import org.koin.compose.koinInject

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.github.openflocon.domain.analytics.usecase.ObserveCurrentDeviceAnalyticsContentUseCase
import io.github.openflocon.domain.analytics.usecase.ResetCurrentDeviceSelectedAnalyticsUseCase
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.analytics.delegate.AnalyticsSelectorDelegate
import io.github.openflocon.flocondesktop.features.analytics.model.AnalyticsContentStateUiModel
import io.github.openflocon.flocondesktop.features.analytics.model.AnalyticsRowUiModel

View file

@ -7,7 +7,7 @@ import io.github.openflocon.domain.dashboard.usecase.ObserveCurrentDeviceDashboa
import io.github.openflocon.domain.dashboard.usecase.SendCheckBoxUpdateDeviceDeviceUseCase
import io.github.openflocon.domain.dashboard.usecase.SendClickEventToDeviceDeviceUseCase
import io.github.openflocon.domain.dashboard.usecase.SubmitTextFieldToDeviceDeviceUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.dashboard.delegate.DashboardSelectorDelegate
import io.github.openflocon.flocondesktop.features.dashboard.mapper.toUi
import io.github.openflocon.flocondesktop.features.dashboard.model.DashboardViewState

View file

@ -6,7 +6,7 @@ import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.database.models.DatabaseExecuteSqlResponseDomainModel
import io.github.openflocon.domain.database.usecase.ExecuteDatabaseQueryUseCase
import io.github.openflocon.domain.database.usecase.ObserveLastSuccessQueriesUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.database.delegate.DatabaseSelectorDelegate
import io.github.openflocon.flocondesktop.features.database.model.DatabaseRowUiModel
import io.github.openflocon.flocondesktop.features.database.model.DatabaseScreenState

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.deeplink.usecase.ExecuteDeeplinkUseCase
import io.github.openflocon.domain.deeplink.usecase.ObserveCurrentDeviceDeeplinkUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.deeplinks.mapper.mapToUi
import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkPart
import io.github.openflocon.flocondesktop.features.deeplinks.model.DeeplinkViewState

View file

@ -10,7 +10,7 @@ import io.github.openflocon.domain.files.usecase.DeleteFileUseCase
import io.github.openflocon.domain.files.usecase.DeleteFolderContentUseCase
import io.github.openflocon.domain.files.usecase.ObserveFolderContentUseCase
import io.github.openflocon.domain.files.usecase.RefreshFolderContentUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.files.mapper.buildContextualActions
import io.github.openflocon.flocondesktop.features.files.mapper.toDomain
import io.github.openflocon.flocondesktop.features.files.mapper.toUi

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.images.usecase.ObserveImagesUseCase
import io.github.openflocon.domain.images.usecase.ResetCurrentDeviceImagesUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.images.model.ImagesStateUiModel
import io.github.openflocon.flocondesktop.features.images.model.ImagesUiModel
import io.github.openflocon.flocondesktop.features.network.mapper.formatTimestamp

View file

@ -10,7 +10,7 @@ import io.github.openflocon.domain.network.usecase.mocks.GenerateNetworkMockFrom
import io.github.openflocon.domain.network.usecase.mocks.GetNetworkMockByIdUseCase
import io.github.openflocon.domain.network.usecase.mocks.ObserveNetworkMocksUseCase
import io.github.openflocon.domain.network.usecase.mocks.UpdateNetworkMockIsEnabledUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.network.mapper.toDomain
import io.github.openflocon.flocondesktop.features.network.mapper.toLineUi
import io.github.openflocon.flocondesktop.features.network.mapper.toUi

View file

@ -9,7 +9,7 @@ import io.github.openflocon.domain.network.usecase.ObserveHttpRequestsUseCase
import io.github.openflocon.domain.network.usecase.RemoveHttpRequestUseCase
import io.github.openflocon.domain.network.usecase.RemoveHttpRequestsBeforeUseCase
import io.github.openflocon.domain.network.usecase.ResetCurrentDeviceHttpRequestsUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.network.delegate.HeaderDelegate
import io.github.openflocon.flocondesktop.features.network.mapper.toDetailUi
import io.github.openflocon.flocondesktop.features.network.mapper.toUi

View file

@ -6,7 +6,7 @@ import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.sharedpreference.models.SharedPreferenceRowDomainModel
import io.github.openflocon.domain.sharedpreference.usecase.EditSharedPrefFieldUseCase
import io.github.openflocon.domain.sharedpreference.usecase.ObserveCurrentDeviceSharedPreferenceValuesUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.sharedpreferences.delegate.SharedPrefSelectorDelegate
import io.github.openflocon.flocondesktop.features.sharedpreferences.model.DeviceSharedPrefUiModel
import io.github.openflocon.flocondesktop.features.sharedpreferences.model.SharedPreferencesRowUiModel

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.table.usecase.ObserveCurrentDeviceTableContentUseCase
import io.github.openflocon.domain.table.usecase.ResetCurrentDeviceSelectedTableUseCase
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.flocondesktop.features.network.mapper.formatTimestamp
import io.github.openflocon.flocondesktop.features.table.delegate.TableSelectorDelegate
import io.github.openflocon.flocondesktop.features.table.model.DeviceTableUiModel

View file

@ -165,25 +165,23 @@ private fun MainScreen(
SubScreen.Settings -> {
SettingsScreen(
modifier =
Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
)
}
SubScreen.Deeplinks -> {
DeeplinkScreen(
modifier =
Modifier
.fillMaxSize(),
Modifier
.fillMaxSize(),
)
}
SubScreen.Analytics -> {
AnalyticsScreen(
modifier =
Modifier
.fillMaxSize(),
Modifier
.fillMaxSize(),
)
}
}

View file

@ -6,7 +6,7 @@ import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.settings.repository.SettingsRepository
import io.github.openflocon.domain.settings.usecase.TestAdbUseCase
import io.github.openflocon.flocondesktop.app.InitialSetupStateHolder
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

View file

@ -7,7 +7,7 @@ import io.github.openflocon.domain.common.Failure
import io.github.openflocon.domain.common.Success
import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableDelegate
import io.github.openflocon.flocondesktop.common.coroutines.closeable.CloseableScoped
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.domain.messages.usecase.HandleIncomingMessagesUseCase
import io.github.openflocon.domain.messages.usecase.StartServerUseCase
import kotlinx.coroutines.delay

View file

@ -1,17 +1,27 @@
package io.github.openflocon.flocondesktop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.FrameWindowScope
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Notification
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberTrayState
import androidx.compose.ui.window.rememberWindowState
import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory
import coil3.network.ktor3.KtorNetworkFetcherFactory
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.app_icon_small
import io.github.openflocon.domain.feedback.FeedbackDisplayer
import io.github.openflocon.domain.feedback.FeedbackDisplayerHandler
import io.github.openflocon.flocondesktop.about.AboutScreen
import io.github.openflocon.flocondesktop.window.MIN_WINDOW_HEIGHT
import io.github.openflocon.flocondesktop.window.MIN_WINDOW_WIDTH
@ -20,14 +30,22 @@ import io.github.openflocon.flocondesktop.window.WindowStateSaver
import io.github.openflocon.flocondesktop.window.size
import io.github.openflocon.flocondesktop.window.windowPosition
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.koinInject
import java.awt.Desktop
import java.awt.Dimension
private const val ACTIVATE_TRAY_NOTIFICATION = false
fun main() {
System.setProperty("apple.awt.application.name", "Flocon")
return application {
var openAbout by remember { mutableStateOf(false) }
val savedState = remember { WindowStateSaver.load() }
val windowState = rememberWindowState(
size = savedState.size(),
position = savedState.windowPosition(),
)
Desktop.getDesktop().setAboutHandler {
openAbout = true
@ -41,15 +59,6 @@ fun main() {
}.build()
}
val savedState = remember {
WindowStateSaver.load()
}
val windowState = rememberWindowState(
size = savedState.size(),
position = savedState.windowPosition(),
)
Window(
state = windowState,
onCloseRequest = {
@ -70,8 +79,12 @@ fun main() {
icon = painterResource(Res.drawable.app_icon_small), // Remove black behind icon
) {
window.minimumSize = Dimension(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
App()
if (ACTIVATE_TRAY_NOTIFICATION) {
FloconTray()
}
// TODO later
// FloconMenu()
if (openAbout) {
AboutScreen(
@ -81,3 +94,57 @@ fun main() {
}
}
}
@Composable
private fun FrameWindowScope.FloconMenu() {
var openSettings by remember { mutableStateOf(false) }
MenuBar {
Menu(
text = "Settings"
) {
Item(
text = "Open",
onClick = {
openSettings = true
}
)
}
}
// TODO Later
// if (openSettings) {
// SettingsScreen(
// onCloseRequest = { openSettings = false }
// )
// }
}
@Composable
private fun ApplicationScope.FloconTray() {
val trayState = rememberTrayState()
val feedbackDisplayerHandler = koinInject<FeedbackDisplayerHandler>()
LaunchedEffect(Unit) {
feedbackDisplayerHandler.notificationsToDisplay
.collect { notification ->
trayState.sendNotification(
Notification(
title = notification.title,
message = notification.message,
type = when (notification.type) {
FeedbackDisplayer.NotificationType.None -> Notification.Type.None
FeedbackDisplayer.NotificationType.Info -> Notification.Type.Info
FeedbackDisplayer.NotificationType.Warning -> Notification.Type.Warning
FeedbackDisplayer.NotificationType.Error -> Notification.Type.Error
}
)
)
}
}
Tray(
state = trayState,
icon = painterResource(Res.drawable.app_icon_small)
)
}