Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -18,7 +20,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
Expand Down Expand Up @@ -174,11 +175,8 @@ fun DesktopApp(
// temporarily disconnected — the VM handles disconnection gracefully while preserving
// selectedEndpoint). allDevices is a live collection, updated before the flow fires.
LaunchedEffect(Unit) {
var knownKeys = activeDeviceMonitor.allDevices.map { it.device.toString() }.toSet()
activeDeviceMonitor.onDeviceConnectionStateChange.collect {
val currentKeys = activeDeviceMonitor.allDevices.map { it.device.toString() }.toSet()
(knownKeys - currentKeys).forEach { evictDesktopViewModelsForKey(it) }
knownKeys = currentKeys
activeDeviceMonitor.onDeviceRemoved.collect {
evictDesktopViewModelsForKey(it)
}
}

Expand All @@ -194,13 +192,35 @@ fun DesktopApp(
}
}

@Composable
private fun ScrimOverlay(visible: Boolean, onDismiss: () -> Unit) {
Column(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(animationDuration)),
exit = fadeOut(animationSpec = tween(animationDuration)),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = scrimAlpha))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = onDismiss,
)
)
}
}
}

@Composable
private fun DeviceContent(
device: Device,
strings: Strings
) {
val viewModel = getViewModel<DeviceRootViewModel>(
key = device.toString()
device = device
) { parametersOf(device) }
val state by viewModel.state.collectAsState()

Expand Down Expand Up @@ -282,7 +302,7 @@ private fun ConnectedDeviceLayout(
device = connectedState.activeDevice.device,
onEndpointClicked = { key ->
viewModel.setSelectedEndpoint(key)
state = state.copy(rightPanelTab = RightPanelTab.EndpointDetails)
state = state.copy(rightPanelTab = RightPanelTab.EndpointDetails.takeUnless { key == null })
},
onGlobalControlsClicked = { state = state.copy(globalControlsOpen = true) },
)
Expand Down Expand Up @@ -353,16 +373,8 @@ private fun ConnectedDeviceLayout(
)
}

// Scrim behind preset overlay — Column provides ColumnScope for AnimatedVisibility
Column(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(
visible = presetVisible,
enter = fadeIn(animationSpec = tween(animationDuration)),
exit = fadeOut(animationSpec = tween(animationDuration)),
) {
Box(modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = scrimAlpha)))
}
}
// Scrim behind preset overlay
ScrimOverlay(visible = presetVisible, onDismiss = { state = state.copy(presetOpen = false) })

// Create/Edit Preset overlay — Box provides alignment; Column provides ColumnScope
Box(modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd)) {
Expand Down Expand Up @@ -403,6 +415,9 @@ private fun ConnectedDeviceLayout(
}
}

// Scrim behind global controls overlay
ScrimOverlay(visible = state.globalControlsOpen, onDismiss = { state = state.copy(globalControlsOpen = false) })

// Global Controls overlay — Box provides alignment; Column provides ColumnScope
Box(modifier = Modifier.fillMaxHeight().align(Alignment.CenterEnd)) {
Column(modifier = Modifier.fillMaxHeight()) {
Expand All @@ -422,15 +437,8 @@ private fun ConnectedDeviceLayout(
Surface(
modifier = Modifier
.fillMaxHeight()
.width(state.globalControlsWidthDp.dp)
.shadow(8.dp)
.border(
1.dp,
MaterialTheme.colorScheme.outline,
RoundedCornerShape(topStart = 8.dp),
),
.width(state.globalControlsWidthDp.dp),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 8.dp),
) {
GlobalControlsWidget(
device = connectedState.activeDevice.device,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ import com.apadmi.mockzilla.ui.ui.common.components.StandardTextTooltip
import com.apadmi.mockzilla.ui.ui.common.components.buttons.BaseButton
import com.apadmi.mockzilla.ui.ui.common.components.buttons.SolidButton
import com.apadmi.mockzilla.ui.ui.common.theme.LocalMonoFontFamily
import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint
import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted
import com.apadmi.mockzilla.ui.ui.common.theme.success

Expand Down Expand Up @@ -313,7 +312,7 @@ private fun ConnectionCard(
HorizontalDivider(Modifier.weight(1f), color = colorScheme.outline)
Text(
text = strings.widgets.deviceConnection.connectAutomatically,
color = colorScheme.onSurfaceFaint,
color = colorScheme.onSurfaceMuted,
style = MaterialTheme.typography.labelMedium,
)
HorizontalDivider(Modifier.weight(1f), color = colorScheme.outline)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ import com.apadmi.mockzilla.ui.ui.common.components.PlatformHorizontalScrollbar
import com.apadmi.mockzilla.ui.ui.common.components.PreviewSurface
import com.apadmi.mockzilla.ui.ui.common.theme.LocalMonoFontFamily
import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint
import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceMuted
import com.apadmi.mockzilla.ui.ui.common.theme.success
import com.apadmi.mockzilla.ui.utils.iconButtonSize

import kotlin.Float

Expand Down Expand Up @@ -237,7 +239,7 @@ fun DeviceTabsWidgetContent(
text = LocalStrings.current.widgets.deviceTabs.betaBanner,
modifier = Modifier.align(Alignment.CenterEnd).padding(8.dp),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceFaint
color = MaterialTheme.colorScheme.onSurfaceMuted
)
}
}
Expand Down Expand Up @@ -362,12 +364,9 @@ private fun DeviceTab(
)
}

Box(
modifier = Modifier
.size(12.dp)
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = onClose),
contentAlignment = Alignment.Center,
IconButton(
onClick = onClose,
modifier = Modifier.iconButtonSize(),
) {
Icon(
imageVector = Icons.Filled.Close,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.apadmi.mockzilla.ui.di.utils

import androidx.compose.runtime.Composable
import com.apadmi.mockzilla.ui.engine.device.Device

import com.apadmi.mockzilla.ui.viewmodel.ViewModel as InternalViewModel

Expand All @@ -21,13 +22,14 @@ actual inline fun <reified T : InternalViewModel> Module.viewModel(
@Composable
actual inline fun <reified T : InternalViewModel> getViewModel(
qualifier: Qualifier?,
key: String?,
device: Device?,
keyPrefix: String?,
noinline parameters: ParametersDefinition?
): T = koinViewModel<T>(
qualifier = qualifier,
key = key,
key = keyPrefix + device.toString(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Need handle here as keyPrefix is nullable ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nah not really, it's just a unique key, as long as it's deterministic it doesn't matter if it's the string null or an empty string

@malinduc-apadmi malinduc-apadmi Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Noted! Sounds good!

parameters = parameters,
scope = MockzillaUiKoinContext.koin.scopeRegistry.rootScope
)

actual fun evictDesktopViewModelsForKey(key: String) = Unit
actual fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String?) = Unit
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.apadmi.mockzilla.ui.di.utils

import androidx.compose.runtime.Composable
import com.apadmi.mockzilla.ui.engine.device.Device
import com.apadmi.mockzilla.ui.viewmodel.ViewModel
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
Expand All @@ -16,8 +17,9 @@ expect inline fun <reified T : ViewModel> Module.viewModel(
@Composable
expect inline fun <reified T : ViewModel> getViewModel(
qualifier: Qualifier? = null,
key: String? = null,
device: Device? = null,
keyPrefix: String? = null,
noinline parameters: ParametersDefinition? = null
): T

expect fun evictDesktopViewModelsForKey(key: String)
expect fun evictDesktopViewModelsForKey(device: Device, keyPrefix: String? = null)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface ActiveDeviceMonitor {

// Fires when a device connects / disconnects
val onDeviceConnectionStateChange: Flow<Unit>
val onDeviceRemoved: Flow<Device>
val allDevices: Collection<StatefulDevice>
}

Expand All @@ -35,6 +36,7 @@ internal class ActiveDeviceManagerImpl(
) : ActiveDeviceMonitor, ActiveDeviceSelector {
override val selectedDevice = MutableStateFlow<StatefulDevice?>(null)
override val onDeviceConnectionStateChange = MutableSharedFlow<Unit>(replay = 1)
override val onDeviceRemoved = MutableSharedFlow<Device>(replay = 0)
private val allDevicesInternal = mutableMapOf<Device, StatefulDevice>()
override val allDevices get() = allDevicesInternal.values

Expand Down Expand Up @@ -106,6 +108,7 @@ internal class ActiveDeviceManagerImpl(
}
allDevicesInternal.remove(device)
onDeviceConnectionStateChange.emit(Unit)
onDeviceRemoved.emit(device)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ val EnStrings = Strings(
filterPlaceholderEmpty = "No matches",
statusCodeFallback = "XXX",
applyLabel = "Apply",
appliedLabel = "Set",
appliedLabel = "Applied",
editLabel = "Edit",
forceFailureBannerTitle = "Forced Failure is on.",
forceFailureBannerBody = "Presets are ignored and locked",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,88 +13,49 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

import com.apadmi.mockzilla.ui.ui.common.theme.onSurfaceFaint

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomSlider(
value: Float,
onValueChange: (Float) -> Unit,
steps: Int = 0,
modifier: Modifier = Modifier,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
enabled: Boolean = true,
activeTrackColor: Color? = null,
showThumb: Boolean = true,
onValueChangeFinished: (() -> Unit)? = null,
) {
val colorScheme = MaterialTheme.colorScheme
val activeColor = activeTrackColor ?: colorScheme.primary

Slider(
modifier = modifier,
value = value,
onValueChange = onValueChange,
modifier = modifier.height(24.dp),
enabled = enabled,
onValueChange = { onValueChange(it) },
steps = steps,
valueRange = valueRange,
onValueChangeFinished = onValueChangeFinished,

thumb = {
if (showThumb) {
Box(
modifier = Modifier
.size(22.dp)
.shadow(
elevation = 2.dp,
shape = CircleShape,
clip = false
)
.background(Color.Transparent, CircleShape),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.size(12.dp)
.background(
if (enabled) {
activeColor
} else {
colorScheme.onSurfaceFaint
},
CircleShape
)
)
}
}
Box(
modifier = Modifier
.size(16.dp)
.clip(CircleShape)
.background(colorScheme.primary)
)
},

track = { sliderState ->
track = { state ->
SliderDefaults.Track(
sliderState = sliderState,
modifier = Modifier.height(4.dp),
sliderState = state,
modifier = Modifier.height(2.dp),
colors = SliderDefaults.colors(
activeTrackColor = activeColor,
inactiveTrackColor = colorScheme.outline.copy(alpha = 0.25f),
disabledActiveTrackColor = colorScheme.onSurfaceFaint.copy(alpha = 0.3f),
disabledInactiveTrackColor = colorScheme.outline.copy(alpha = 0.1f),
thumbColor = Color.Transparent,
activeTrackColor = colorScheme.primary,
inactiveTrackColor = colorScheme.outline,
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent,
),
drawStopIndicator = null,
thumbTrackGapSize = 0.dp,
)
},

colors = SliderDefaults.colors(
thumbColor = Color.Transparent,
activeTrackColor = activeColor,
inactiveTrackColor = colorScheme.outline.copy(alpha = 0.25f),
),
}
)
}

Expand All @@ -109,11 +70,5 @@ private fun MockzillaSliderPreview() = PreviewSurface {
value = 0.4f,
onValueChange = {},
)

CustomSlider(
value = 0.7f,
onValueChange = {},
enabled = false,
)
}
}
Loading
Loading