Skip to content
Closed
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
294 changes: 294 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ coil = "3.5.0"
m3 = "1.5.0-alpha26"
nav3 = "1.1.6"
navigationevent = "1.1.2"
# Material Symbols (the M3 icon set) used by the manager UI, matching WeKit. These are
# Compose Multiplatform artifacts; the `-cmp` Android variant is what we actually resolve.
composablehorizons-symbols = "2.2.1"
# Governs every androidx.compose.* artifact below; none of them pin a version.
compose-bom = "2026.08.00"

Expand Down Expand Up @@ -44,6 +47,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "m3" }
androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "m3" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
composablehorizons-material-symbols-outlined = { module = "com.composables:icons-material-symbols-outlined-cmp", version.ref = "composablehorizons-symbols" }

# Navigation 3. Stable since Nov 2025; the back stack is a plain observable list
# of NavKey objects rather than route strings.
Expand Down Expand Up @@ -85,6 +89,7 @@ compose = [
"androidx-compose-material3",
"androidx-compose-material3-adaptive-navigation-suite",
"androidx-compose-material-icons-extended",
"composablehorizons-material-symbols-outlined",
"androidx-lifecycle-viewmodel-compose",
"androidx-navigation3-runtime",
"androidx-navigation3-ui",
Expand Down
16 changes: 15 additions & 1 deletion manager-ui/src/main/kotlin/org/matrix/vector/ui/ApiBadge.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package org.matrix.vector.ui

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand All @@ -24,17 +27,28 @@ import androidx.compose.ui.unit.sp
@Composable
fun ApiBadge(label: String, value: String, incompatible: Boolean = false) {
val colors = MaterialTheme.colorScheme
Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(3.dp)) {
// Single line, never wrapping: the badge sits in a fixed-width column, and a wrapping badge
// (e.g. "LSPosed\n102") would break the shared name/description start that the fixed column
// exists to keep. The scale name and number stay on one line however narrow the column is.
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(3.dp),
modifier = Modifier.width(IntrinsicSize.Max),
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp),
color = colors.onSurfaceVariant.copy(alpha = 0.7f),
maxLines = 1,
softWrap = false,
)
Text(
text = value,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold,
color = if (incompatible) colors.error else colors.primary,
maxLines = 1,
softWrap = false,
)
}
}
48 changes: 48 additions & 0 deletions manager-ui/src/main/kotlin/org/matrix/vector/ui/CheckSwitch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.matrix.vector.ui

import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

/**
* A Material 3 switch that shows a **check when on and a cross when off** inside the thumb.
*
* The ordinary Material 3 `Switch` is a slider whose thumb colour alone says which state it is in;
* a row where the thumb is the only clue is hard to read at a glance, and a module list with a
* switch per row makes "read the colour" the whole interaction. Putting the mark in the thumb keeps
* the switch's Material 3 skeleton (the track, the shape, the motion) while the state is legible in
* the mark itself.
*
* It is a thin wrapper: all of the switch's own parameters are forwarded unchanged, and only
* [thumbContent] is added. The mark colour follows the switch's `iconColor`, so the check and the
* cross pick up the same themed colour the switch would have used for its thumb.
*/
@Composable
fun CheckSwitch(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
thumbContent = {
Icon(
imageVector = if (checked) Icons.Rounded.Check else Icons.Rounded.Close,
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
},
)
}
37 changes: 29 additions & 8 deletions manager-ui/src/main/kotlin/org/matrix/vector/ui/ModuleRow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ import androidx.compose.ui.unit.dp
/** The module's icon, and the slot it is drawn in whether or not it is selected. */
private val ICON_SIZE = 48.dp

/**
* The fixed width of the icon column (the icon plus the API badge under it).
*
* Deliberately wider than the icon: the badge reads "LSPosed 102" on a modern module and "Xposed
* 93" on a legacy one, and if the column wrapped its contents the text column to its right would
* start at a different x for every row — the names and descriptions would stop lining up. Fixing
* the column width wide enough for the widest badge (single-line, so a "LSPosed 102" fits) is what
* keeps the name/description edges shared across the list without the badge truncating.
*/
private val ICON_COLUMN_WIDTH = 66.dp

/** Room for a version and its mark. Anything longer scrolls past instead of pushing. */
private val VERSION_WIDTH = 104.dp

Expand Down Expand Up @@ -131,18 +142,28 @@ fun ModuleRow(
// The icon is the selection handle. Double-tapping it is the host's chance to toggle without
// leaving the list; a bare tap only reports state, since a one-tap toggle would fire whenever
// a thumb brushed the list.
//
// The column is fixed at the icon's width so the API badge underneath it cannot widen it: a
// wide badge (e.g. "liblsposed 102") would otherwise push the text column right for that row
// alone, and the module names would no longer line up. The badge is laid out at its natural
// width inside the fixed box — if it is wider than the icon it overflows to the right but the
// text column keeps its fixed start, which is the alignment the badge would otherwise break.
Column(
modifier =
if (onIconClick != null)
(if (onIconClick != null)
Modifier.contextClickable(onClick = onIconClick, onLongClick = onIconLongClick)
else Modifier,
// Against the text, not centred over the badge: the badge below is wider than the icon,
// so centring left a gap between the icon and the edge the names all start from.
horizontalAlignment = Alignment.End,
else Modifier)
.width(ICON_COLUMN_WIDTH),
// Left-aligned with the text: the icon's own left edge sits on the same vertical line the
// names start from, so every row's icon and title line up regardless of how the icon was
// drawn (some module icons carry their own padding, which previously pushed them inward
// and made the list look ragged).
horizontalAlignment = Alignment.Start,
) {
// Fixed at the icon's size whatever is drawn inside, so selecting a module cannot resize
// its row — a tick larger than the icon would grow this box and reflow the list.
Box(modifier = Modifier.size(ICON_SIZE), contentAlignment = Alignment.Center) {
// its row — a tick larger than the icon would grow this box and reflow the list. Pinned
// to the top-start so the icon, not a centred one, is left-aligned against the edge.
Box(modifier = Modifier.size(ICON_SIZE), contentAlignment = Alignment.TopStart) {
icon()
if (selected) {
Box(
Expand All @@ -165,7 +186,7 @@ fun ModuleRow(
apiBadge()
}

Spacer(Modifier.width(16.dp))
Spacer(Modifier.width(8.dp))

// A Box, not a third column: reserving a column for the version and reach would take width
// from every line of the description whether or not anything was there. They overlap the text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -118,7 +117,7 @@ fun ToggleRow(
),
supportingContent = subtitle?.let { { Text(it) } },
leadingContent = { Icon(icon, contentDescription = null) },
trailingContent = { Switch(checked = checked, onCheckedChange = null) },
trailingContent = { CheckSwitch(checked = checked, onCheckedChange = null) },
colors = sheetRowColors,
) { Text(title) }
}
Expand Down
33 changes: 21 additions & 12 deletions manager-ui/src/main/kotlin/org/matrix/vector/ui/logs/LogsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ import androidx.compose.material.icons.rounded.UnfoldLess
import androidx.compose.material.icons.rounded.UnfoldMore
import androidx.compose.material.icons.automirrored.rounded.Label
import androidx.compose.material.icons.rounded.SearchOff
import androidx.compose.material.icons.rounded.VerticalAlignBottom
import androidx.compose.material.icons.rounded.VerticalAlignTop
import androidx.compose.material.icons.rounded.KeyboardDoubleArrowDown
import androidx.compose.material.icons.rounded.KeyboardDoubleArrowUp
import androidx.compose.material.icons.rounded.WarningAmber
import androidx.compose.material.icons.automirrored.rounded.WrapText
import androidx.compose.material3.CircularProgressIndicator
Expand All @@ -65,12 +65,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.InputChip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
Expand Down Expand Up @@ -107,6 +106,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import org.matrix.vector.ui.CheckSwitch
import org.matrix.vector.ui.LocalDialogLocalizer
import org.matrix.vector.ui.PanelHeader
import org.matrix.vector.ui.SearchField
Expand Down Expand Up @@ -355,7 +355,7 @@ private fun LogPane(
LaunchedEffect(state.scroll?.token, jumpInset) {
val command = state.scroll ?: return@LaunchedEffect
if (state.rows.isNotEmpty()) {
listState.scrollToItem(command.position.coerceIn(0, state.rows.lastIndex))
listState.animateScrollToItem(command.position.coerceIn(0, state.rows.lastIndex))
}
}

Expand Down Expand Up @@ -544,23 +544,32 @@ private fun LogList(
// file: hiding one would change the container's height, which is the list's bottom inset,
// and so shift the log under the reader as a side effect of scrolling.
if (showJump) {
Row(
Column(
modifier =
Modifier.align(Alignment.BottomEnd)
.onSizeChanged { onJumpInset(it.height) }
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
SmallFloatingActionButton(onClick = { viewModel.jumpToOldest(tab) }) {
FilledTonalIconButton(
onClick = { viewModel.jumpToOldest(tab) },
modifier = Modifier.size(40.dp),
) {
Icon(
Icons.Rounded.VerticalAlignTop,
Icons.Rounded.KeyboardDoubleArrowUp,
contentDescription = stringResource(R.string.logs_jump_oldest),
modifier = Modifier.size(20.dp),
)
}
SmallFloatingActionButton(onClick = { viewModel.jumpToNewest(tab) }) {
FilledTonalIconButton(
onClick = { viewModel.jumpToNewest(tab) },
modifier = Modifier.size(40.dp),
) {
Icon(
Icons.Rounded.VerticalAlignBottom,
Icons.Rounded.KeyboardDoubleArrowDown,
contentDescription = stringResource(R.string.logs_jump_newest),
modifier = Modifier.size(20.dp),
)
}
}
Expand Down Expand Up @@ -790,7 +799,7 @@ private fun LogSettingsSheet(
)
},
trailingContent = {
Switch(checked = enabled, onCheckedChange = { viewModel.setVerbose(it) })
CheckSwitch(checked = enabled, onCheckedChange = { viewModel.setVerbose(it) })
},
colors = sheetRowColors,
) { Text(stringResource(R.string.logs_verbose_switch)) }
Expand Down
Loading