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
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.github.openflocon.flocondesktop.common.ui.window

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.app_icon
import io.github.openflocon.library.designsystem.components.escape.LocalEscapeHandlerStack
import org.jetbrains.compose.resources.painterResource

data class FloconWindowStateDesktop(
val windowState: WindowState,
) : FloconWindowState

actual fun createFloconWindowState(size: DpSize?): FloconWindowState = FloconWindowStateDesktop(
WindowState(
placement = WindowPlacement.Floating,
position = WindowPosition(Alignment.Center),
size = size ?: defaultWindowSize
)
)

// TODO Use this component for main window too
@Composable
actual fun FloconWindow(
title: String,
state: FloconWindowState,
onCloseRequest: () -> Unit,
alwaysOnTop: Boolean,
content: @Composable () -> Unit,
) {
val handlers = remember { mutableStateListOf<() -> Boolean>() }

Window(
title = title,
icon = painterResource(Res.drawable.app_icon),
state = (state as FloconWindowStateDesktop).windowState,
alwaysOnTop = alwaysOnTop,
onPreviewKeyEvent = {
when (it.key) {
Key.Escape if (it.type == KeyEventType.KeyDown) -> handlers.lastOrNull()?.invoke() ?: false

else -> false
}
},
onCloseRequest = onCloseRequest,
) {
CompositionLocalProvider(LocalEscapeHandlerStack provides handlers) {
content()
}
}
}
1 change: 1 addition & 0 deletions FloconDesktop/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ kotlin {
implementation(libs.androidx.paging.common)
implementation(libs.androidx.paging.compose)
implementation(libs.markdown.renderer)
implementation(libs.koalaplot.core)

// TODO Remove
implementation(projects.data.remote)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ internal sealed interface AppAction {
data object Restart : AppAction

data object Screenshoot : AppAction
data object Performance : AppAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.github.openflocon.flocondesktop.features.deeplinks.deeplinkRoutes
import io.github.openflocon.flocondesktop.features.files.filesRoutes
import io.github.openflocon.flocondesktop.features.images.imageRoutes
import io.github.openflocon.flocondesktop.features.network.networkRoutes
import io.github.openflocon.flocondesktop.features.performance.performanceRoutes
import io.github.openflocon.flocondesktop.features.sharedpreferences.sharedPreferencesRoutes
import io.github.openflocon.flocondesktop.features.table.tableRoutes
import io.github.openflocon.library.designsystem.FloconTheme
Expand Down Expand Up @@ -79,6 +80,7 @@ private fun Content(
onAppSelected = { onAction(AppAction.SelectApp(it)) },
onRecordClicked = { onAction(AppAction.Record) },
onRestartClicked = { onAction(AppAction.Restart) },
onPerformanceClicked = { onAction(AppAction.Performance) },
onTakeScreenshotClicked = { onAction(AppAction.Screenshoot) }
)
}
Expand All @@ -100,5 +102,6 @@ private fun Content(
tableRoutes()
settingsRoutes()
crashReporterRoutes()
performanceRoutes()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.github.openflocon.flocondesktop.features.deeplinks.DeeplinkRoutes
import io.github.openflocon.flocondesktop.features.files.FilesRoutes
import io.github.openflocon.flocondesktop.features.images.ImageRoutes
import io.github.openflocon.flocondesktop.features.network.NetworkRoutes
import io.github.openflocon.flocondesktop.features.performance.PerformanceRoutes
import io.github.openflocon.flocondesktop.features.sharedpreferences.SharedPreferencesRoutes
import io.github.openflocon.flocondesktop.features.table.TableRoutes
import io.github.openflocon.flocondesktop.messages.ui.MessagesServerDelegate
Expand Down Expand Up @@ -110,6 +111,7 @@ internal class AppViewModel(
AppAction.Record -> onRecord()
AppAction.Restart -> onRestart()
AppAction.Screenshoot -> onTakeScreenshot()
AppAction.Performance -> onPerformance()
is AppAction.SelectApp -> onAppSelected(action)
is AppAction.SelectDevice -> onDeviceSelected(action)
}
Expand Down Expand Up @@ -182,4 +184,8 @@ internal class AppViewModel(
)
}
}

private fun onPerformance() {
navigationState.navigate(PerformanceRoutes.Performance)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fun MainScreenTopBar(
recordState: RecordVideoStateUiModel,
onRecordClicked: () -> Unit,
onRestartClicked: () -> Unit,
onPerformanceClicked: () -> Unit,
) {
Row(
modifier = modifier
Expand All @@ -64,6 +65,7 @@ fun MainScreenTopBar(
recordState = recordState,
onRecordClicked = onRecordClicked,
onRestartClicked = onRestartClicked,
onPerformanceClicked = onPerformanceClicked,
devicesState = devicesState,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.openflocon.flocondesktop.app.ui.view.topbar.actions
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Analytics
import androidx.compose.material.icons.outlined.CameraAlt
import androidx.compose.material.icons.outlined.RestartAlt
import androidx.compose.material.icons.outlined.StopCircle
Expand All @@ -21,6 +22,7 @@ internal fun TopBarActions(
devicesState: DevicesStateUiModel,
onRecordClicked: () -> Unit,
onRestartClicked: () -> Unit,
onPerformanceClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
Expand Down Expand Up @@ -52,5 +54,12 @@ internal fun TopBarActions(
onClicked = onTakeScreenshotClicked,
isEnabled = devicesState.deviceSelected?.canScreenshot == true && devicesState.deviceSelected?.isActive == true,
)
TopBarButton(
active = false,
imageVector = Icons.Outlined.Analytics,
contentDescription = "performances",
onClicked = onPerformanceClicked,
isEnabled = true,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.github.openflocon.flocondesktop.features.deeplinks.deeplinkModule
import io.github.openflocon.flocondesktop.features.files.filesModule
import io.github.openflocon.flocondesktop.features.images.imagesModule
import io.github.openflocon.flocondesktop.features.network.networkModule
import io.github.openflocon.flocondesktop.features.performance.performanceModule
import io.github.openflocon.flocondesktop.features.sharedpreferences.sharedPreferencesModule
import io.github.openflocon.flocondesktop.features.table.tableModule
import io.github.openflocon.flocondesktop.messages.di.messagesModule
Expand All @@ -28,5 +29,6 @@ val featuresModule = module {
deeplinkModule,
settingsModule,
crashReporterModule,
performanceModule,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.openflocon.flocondesktop.features.performance

import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel
import org.koin.compose.viewmodel.dsl.viewModelOf
import org.koin.dsl.module

val performanceModule = module {
singleOf(::PerformanceMetricsRepository)
viewModelOf(::PerformanceViewModel)
viewModel { (event: MetricEventUiModel) -> PerformanceDetailViewModel(event, get()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.openflocon.flocondesktop.features.performance

import kotlinx.serialization.Serializable

@Serializable
data class MetricEventUiModel(
val timestamp: String,
val ramMb: String?,
val rawRamMb: Long?,
val fps: String,
val rawFps: Double,
val jankPercentage: String,
val rawJankPercentage: Double,
val battery: String,
val screenshotPath: String?,
val isFpsDrop: Boolean,
)

fun previewMetricsEvent() = MetricEventUiModel(
timestamp = "10:55:38.123",
ramMb = "150",
rawRamMb = 150L,
fps = "60.0",
rawFps = 60.0,
jankPercentage = "0.0%",
rawJankPercentage = 0.0,
battery = "85%",
screenshotPath = null,
isFpsDrop = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.github.openflocon.flocondesktop.features.performance

import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.EntryProviderScope
import io.github.openflocon.flocondesktop.features.performance.view.PerformanceDetailScreen
import io.github.openflocon.flocondesktop.features.performance.view.PerformanceScreen
import io.github.openflocon.navigation.FloconRoute
import io.github.openflocon.navigation.WindowRoute
import io.github.openflocon.navigation.scene.WindowSceneStrategy
import kotlinx.serialization.Serializable

internal sealed interface PerformanceRoutes : FloconRoute {

@Serializable
data object Performance : PerformanceRoutes, WindowRoute {
override val singleTopKey = "performance"
}

@Serializable
data class Detail(val event: MetricEventUiModel) : PerformanceRoutes, WindowRoute {
override val singleTopKey = null
}
}

fun EntryProviderScope<FloconRoute>.performanceRoutes() {
entry<PerformanceRoutes.Performance>(
metadata = WindowSceneStrategy.window(
size = DpSize(
width = 1000.dp,
height = 800.dp
)
)
) {
PerformanceScreen()
}
entry<PerformanceRoutes.Detail>(
metadata = WindowSceneStrategy.window(
size = DpSize(
width = 800.dp,
height = 1000.dp
)
)
) {
PerformanceDetailScreen(initialEvent = it.event)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.github.openflocon.flocondesktop.features.performance

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

class PerformanceDetailViewModel(
initialEvent: MetricEventUiModel,
private val repository: PerformanceMetricsRepository,
) : ViewModel() {

private val _event = MutableStateFlow(initialEvent)
val event = _event.asStateFlow()

private val _hasNext = MutableStateFlow(false)
val hasNext = _hasNext.asStateFlow()

private val _hasPrevious = MutableStateFlow(false)
val hasPrevious = _hasPrevious.asStateFlow()

init {
updateNavigationState()
}

fun onNext() {
val metrics = repository.getMetrics()
val currentIndex = metrics.indexOfFirst { it.timestamp == _event.value.timestamp }
if (currentIndex > 0) {
_event.value = metrics[currentIndex - 1]
updateNavigationState()
}
}

fun onPrevious() {
val metrics = repository.getMetrics()
val currentIndex = metrics.indexOfFirst { it.timestamp == _event.value.timestamp }
if (currentIndex != -1 && currentIndex < metrics.size - 1) {
_event.value = metrics[currentIndex + 1]
updateNavigationState()
}
}
Comment on lines +26 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The onNext and onPrevious functions are inefficient as they fetch the full list of metrics from the repository and find the current index on every invocation. Since the list of metrics can grow, this should be optimized.

Additionally, the naming is confusing. onNext moves to a newer event (lower index), while onPrevious moves to an older event (higher index). The UI uses forward/back arrows which might be interpreted differently by users.

Consider caching the list of metrics and the current index within the ViewModel. You could also rename the functions to be more explicit, like onNewerEvent() and onOlderEvent() to avoid ambiguity.


private fun updateNavigationState() {
val metrics = repository.getMetrics()
val currentIndex = metrics.indexOfFirst { it.timestamp == _event.value.timestamp }
_hasNext.value = currentIndex > 0
_hasPrevious.value = currentIndex != -1 && currentIndex < metrics.size - 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.openflocon.flocondesktop.features.performance

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

class PerformanceMetricsRepository {
private val _metrics = MutableStateFlow<List<MetricEventUiModel>>(emptyList())
val metrics = _metrics.asStateFlow()

fun addMetric(metric: MetricEventUiModel) {
_metrics.update { listOf(metric) + it }
}

fun clear() {
_metrics.value = emptyList()
}
Comment on lines +8 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of addMetric using listOf(metric) + it is inefficient as it creates a new list and copies all elements on every update. This can become a performance bottleneck for a monitoring tool.

Using a kotlinx.collections.immutable.PersistentList would make prepending an element much more efficient (O(log n) vs O(n)). I've also updated clear() to work with the new type. You will need to add the required import for PersistentList.

Suggested change
private val _metrics = MutableStateFlow<List<MetricEventUiModel>>(emptyList())
val metrics = _metrics.asStateFlow()
fun addMetric(metric: MetricEventUiModel) {
_metrics.update { listOf(metric) + it }
}
fun clear() {
_metrics.value = emptyList()
}
private val _metrics = MutableStateFlow<kotlinx.collections.immutable.PersistentList<MetricEventUiModel>>(kotlinx.collections.immutable.persistentListOf())
val metrics = _metrics.asStateFlow()
fun addMetric(metric: MetricEventUiModel) {
_metrics.update { it.add(0, metric) }
}
fun clear() {
_metrics.value = kotlinx.collections.immutable.persistentListOf()
}


fun getMetrics() = _metrics.value
}
Loading