Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
<string name="new_client_version">New client version available: %1$s</string>
<string name="new_client_version_desc">Don’t forget to update the app version\n(current: %1$s)</string>
<string name="new_desktop_version">New destkop version available: %1$s</string>
<string name="update_available_desktop">Update: %1$s</string>
<string name="update_available_client">SDK update: %1$s</string>
<string name="no_analytics">No analytics</string>
<string name="no_devices_found">No Devices Found</string>
<string name="open_external">Open in external editor</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.scene.SinglePaneSceneStrategy
import io.github.openflocon.flocondesktop.app.ui.settings.settingsRoutes
import io.github.openflocon.flocondesktop.app.ui.view.leftpannel.LeftPanelView
import io.github.openflocon.flocondesktop.app.ui.view.topbar.MainScreenTopBar
import io.github.openflocon.flocondesktop.app.version.VersionCheckerViewModel
import io.github.openflocon.flocondesktop.app.version.VersionCheckerView
import io.github.openflocon.flocondesktop.common.ui.feedback.FeedbackDisplayerView
import io.github.openflocon.flocondesktop.features.adbcommander.adbCommanderRoutes
Expand All @@ -38,12 +40,17 @@ import org.koin.compose.viewmodel.koinViewModel
fun AppScreen() {
val viewModel = koinViewModel<AppViewModel>()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val versionCheckerViewModel = koinViewModel<VersionCheckerViewModel>()
val updateChip by versionCheckerViewModel.updateChip.collectAsStateWithLifecycle()
val uriHandler = LocalUriHandler.current

Box(modifier = Modifier.fillMaxSize()) {
Content(
uiState = uiState,
navigationState = viewModel.navigationState,
onAction = viewModel::onAction
onAction = viewModel::onAction,
updateChip = updateChip,
onUpdateChipClicked = { uriHandler.openUri(it.link) },
)
FeedbackDisplayerView()
VersionCheckerView()
Expand All @@ -54,7 +61,9 @@ fun AppScreen() {
private fun Content(
uiState: AppUiState,
navigationState: MainFloconNavigationState,
onAction: (AppAction) -> Unit
onAction: (AppAction) -> Unit,
updateChip: VersionCheckerViewModel.UpdateChipUiModel?,
onUpdateChipClicked: (VersionCheckerViewModel.UpdateChipUiModel) -> Unit,
) {
val sceneStrategies = remember {
listOf(
Expand Down Expand Up @@ -90,7 +99,9 @@ private fun Content(
onAppSelected = { onAction(AppAction.SelectApp(it)) },
onRecordClicked = { onAction(AppAction.Record) },
onRestartClicked = { onAction(AppAction.Restart) },
onTakeScreenshotClicked = { onAction(AppAction.Screenshoot) }
onTakeScreenshotClicked = { onAction(AppAction.Screenshoot) },
updateChip = updateChip,
onUpdateChipClicked = onUpdateChipClicked,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ package io.github.openflocon.flocondesktop.app.ui.view.topbar

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowCircleUp
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -25,6 +34,7 @@ import io.github.openflocon.flocondesktop.app.ui.model.DeviceItemUiModel
import io.github.openflocon.flocondesktop.app.ui.model.DevicesStateUiModel
import io.github.openflocon.flocondesktop.app.ui.model.RecordVideoStateUiModel
import io.github.openflocon.flocondesktop.app.ui.view.topbar.actions.TopBarActions
import io.github.openflocon.flocondesktop.app.version.VersionCheckerViewModel
import io.github.openflocon.library.designsystem.FloconTheme
import org.jetbrains.compose.resources.painterResource

Expand All @@ -41,14 +51,19 @@ fun MainScreenTopBar(
recordState: RecordVideoStateUiModel,
onRecordClicked: () -> Unit,
onRestartClicked: () -> Unit,
updateChip: VersionCheckerViewModel.UpdateChipUiModel? = null,
onUpdateChipClicked: (VersionCheckerViewModel.UpdateChipUiModel) -> Unit = {},
) {
Row(
modifier = modifier
.background(FloconTheme.colorPalette.surface)
.padding(vertical = 8.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Title()
Title(
updateChip = updateChip,
onUpdateChipClicked = onUpdateChipClicked,
)
Spacer(modifier = Modifier.width(18.dp))
TopBarDeviceAndAppView(
devicesState = devicesState,
Expand All @@ -69,9 +84,49 @@ fun MainScreenTopBar(
}
}

@Composable
private fun TopBarUpdateChip(
uiModel: VersionCheckerViewModel.UpdateChipUiModel,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.background(FloconTheme.colorPalette.accent.copy(alpha = 0.2f))
.border(
width = 1.dp,
color = FloconTheme.colorPalette.onAccent.copy(alpha = 0.4f),
shape = RoundedCornerShape(4.dp),
)
.clickable(onClick = onClick)
.pointerHoverIcon(PointerIcon.Hand)
.padding(horizontal = 6.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
imageVector = Icons.Outlined.ArrowCircleUp,
contentDescription = null,
tint = FloconTheme.colorPalette.onAccent,
modifier = Modifier.size(12.dp),
)
Text(
text = uiModel.text,
color = FloconTheme.colorPalette.onAccent,
style = FloconTheme.typography.labelSmall.copy(
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
),
)
}
}

@Composable
private fun Title(
modifier: Modifier = Modifier,
updateChip: VersionCheckerViewModel.UpdateChipUiModel? = null,
onUpdateChipClicked: (VersionCheckerViewModel.UpdateChipUiModel) -> Unit = {},
) {
Row(
modifier = modifier,
Expand All @@ -86,13 +141,25 @@ private fun Title(
contentDescription = "Description de mon image",
)

Text(
text = "Flocon",
style = FloconTheme.typography.titleSmall.copy(
fontSize = 18.sp,
color = FloconTheme.colorPalette.onSurface,
fontWeight = FontWeight.SemiBold,
),
)
Column(
verticalArrangement = Arrangement.Center,
) {
Text(
text = "Flocon",
style = FloconTheme.typography.titleSmall.copy(
fontSize = 18.sp,
color = FloconTheme.colorPalette.onSurface,
fontWeight = FontWeight.SemiBold,
),
)

if (updateChip != null) {
Spacer(modifier = Modifier.height(2.dp))
TopBarUpdateChip(
uiModel = updateChip,
onClick = { onUpdateChipClicked(updateChip) },
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.new_client_version
import flocondesktop.composeapp.generated.resources.new_client_version_desc
import flocondesktop.composeapp.generated.resources.new_desktop_version
import flocondesktop.composeapp.generated.resources.update_available_client
import flocondesktop.composeapp.generated.resources.update_available_desktop
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.versions.model.IsLastVersionDomainModel
import io.github.openflocon.domain.versions.usecase.CheckIsDesktopOnLastVersionUseCase
import io.github.openflocon.domain.versions.usecase.DismissClientVersionUseCase
import io.github.openflocon.domain.versions.usecase.DismissDesktopVersionUseCase
import io.github.openflocon.domain.versions.usecase.ObserveIsClientOnLastVersionUseCase
import io.github.openflocon.domain.versions.usecase.ObserveUpdateAvailableUseCase
import io.github.openflocon.flocondesktop.BuildConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
Expand All @@ -22,10 +29,18 @@ import org.jetbrains.compose.resources.getString

class VersionCheckerViewModel(
private val checkIsDesktopOnLastVersionUseCase: CheckIsDesktopOnLastVersionUseCase,
private val dismissDesktopVersionUseCase: DismissDesktopVersionUseCase,
private val dismissClientVersionUseCase: DismissClientVersionUseCase,
private val observeIsClientOnLastVersionUseCase: ObserveIsClientOnLastVersionUseCase,
private val observeUpdateAvailableUseCase: ObserveUpdateAvailableUseCase,
private val dispatcherProvider: DispatcherProvider,
) : ViewModel() {

data class UpdateChipUiModel(
val text: String,
val link: String,
)

data class VersionAvailableUiModel(
val version: String,
val link: String,
Expand All @@ -38,6 +53,31 @@ class VersionCheckerViewModel(
val client: VersionAvailableUiModel?,
)

val updateChip: StateFlow<UpdateChipUiModel?> = observeUpdateAvailableUseCase(BuildConfig.APP_VERSION)
.map { update ->
when (update) {
is ObserveUpdateAvailableUseCase.UpdateAvailableDomainModel.DesktopUpdate -> {
UpdateChipUiModel(
text = getString(Res.string.update_available_desktop, update.version),
link = update.link,
)
}
is ObserveUpdateAvailableUseCase.UpdateAvailableDomainModel.ClientUpdate -> {
UpdateChipUiModel(
text = getString(Res.string.update_available_client, update.version),
link = update.link,
)
}
ObserveUpdateAvailableUseCase.UpdateAvailableDomainModel.None -> null
}
}
.flowOn(dispatcherProvider.viewModel)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null,
)

private val hiddenClientDialogs = MutableStateFlow<Set<VersionAvailableUiModel>>(emptySet())
private val desktopVersionAvailable = MutableStateFlow<VersionAvailableUiModel?>(null)

Expand Down Expand Up @@ -108,9 +148,15 @@ class VersionCheckerViewModel(

fun hideDesktopNewVersionDialog(uimodel: VersionAvailableUiModel) {
desktopVersionAvailable.update { null }
viewModelScope.launch(dispatcherProvider.viewModel) {
dismissDesktopVersionUseCase(uimodel.version)
}
}

fun hideClientNewVersionDialog(uimodel: VersionAvailableUiModel) {
hiddenClientDialogs.update { it + uimodel }
viewModelScope.launch(dispatcherProvider.viewModel) {
dismissClientVersionUseCase(uimodel.version)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,18 @@ internal class SettingsRepositoryImpl(
override suspend fun setTheme(value: ThemeSetting) {
localSettingsDataSource.setTheme(value)
}

override fun getDismissedDesktopVersion(): String? = localSettingsDataSource.getDismissedDesktopVersion()

override suspend fun setDismissedDesktopVersion(version: String) {
localSettingsDataSource.setDismissedDesktopVersion(version)
}

override val dismissedClientVersionFlow: Flow<String?> = localSettingsDataSource.dismissedClientVersionFlow

override fun getDismissedClientVersion(): String? = localSettingsDataSource.getDismissedClientVersion()

override suspend fun setDismissedClientVersion(version: String) {
localSettingsDataSource.setDismissedClientVersion(version)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ internal interface SettingsDataSource {
suspend fun setFontSizeMultiplier(value: Float)
suspend fun setTheme(value: ThemeSetting)

fun getDismissedDesktopVersion(): String?
suspend fun setDismissedDesktopVersion(version: String)

fun getDismissedClientVersion(): String?
suspend fun setDismissedClientVersion(version: String)

val adbPath: Flow<String?>
val fontSizeMultiplier: StateFlow<Float>
val theme: StateFlow<ThemeSetting>
val dismissedClientVersionFlow: Flow<String?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ internal class SettingsDataSourcePrefs(
settings.putString(THEME, value.name)
}

override fun getDismissedDesktopVersion(): String? = settings.getStringOrNull(DISMISSED_DESKTOP_VERSION)

override suspend fun setDismissedDesktopVersion(version: String) {
settings.putString(DISMISSED_DESKTOP_VERSION, version)
}

override val dismissedClientVersionFlow: Flow<String?> = flowSettings.getStringOrNullFlow(DISMISSED_CLIENT_VERSION)

override fun getDismissedClientVersion(): String? = settings.getStringOrNull(DISMISSED_CLIENT_VERSION)

override suspend fun setDismissedClientVersion(version: String) {
settings.putString(DISMISSED_CLIENT_VERSION, version)
}

private fun String?.toThemeSetting(): ThemeSetting = this
?.let { name -> ThemeSetting.entries.firstOrNull { it.name == name } }
?: ThemeSetting.DEFAULT
Expand Down Expand Up @@ -102,6 +116,8 @@ internal class SettingsDataSourcePrefs(
private const val ADB_PATH = "adb_path"
private const val FONT_SIZE_MULTIPLIER = "font_size_multiplier"
private const val THEME = "theme"
private const val DISMISSED_DESKTOP_VERSION = "dismissed_desktop_version"
private const val DISMISSED_CLIENT_VERSION = "dismissed_client_version"

private const val NETWORK_SETTINGS = "network_settings"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ interface SettingsRepository {

suspend fun setTheme(value: ThemeSetting)

fun getDismissedDesktopVersion(): String?

suspend fun setDismissedDesktopVersion(version: String)

val dismissedClientVersionFlow: Flow<String?>

fun getDismissedClientVersion(): String?

suspend fun setDismissedClientVersion(version: String)

val adbPath: Flow<String?>
val fontSizeMultiplier: StateFlow<Float>
val theme: StateFlow<ThemeSetting>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package io.github.openflocon.domain.versions

import io.github.openflocon.domain.versions.usecase.CheckIsDesktopOnLastVersionUseCase
import io.github.openflocon.domain.versions.usecase.DismissClientVersionUseCase
import io.github.openflocon.domain.versions.usecase.DismissDesktopVersionUseCase
import io.github.openflocon.domain.versions.usecase.ObserveIsClientOnLastVersionUseCase
import io.github.openflocon.domain.versions.usecase.ObserveLastAvailableFloconVersionUseCase
import io.github.openflocon.domain.versions.usecase.ObserveUpdateAvailableUseCase
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module

internal val versionModule = module {
factoryOf(::CheckIsDesktopOnLastVersionUseCase)
factoryOf(::DismissDesktopVersionUseCase)
factoryOf(::DismissClientVersionUseCase)
factoryOf(::ObserveLastAvailableFloconVersionUseCase)
factoryOf(::ObserveIsClientOnLastVersionUseCase)
factoryOf(::ObserveUpdateAvailableUseCase)
}
Loading