refactor(android): compact drafts and body measurement screens

This commit is contained in:
fy59 2026-09-20 14:26:34 +02:00
parent 53baa7ad24
commit f27ef5af5f
8 changed files with 357 additions and 190 deletions

View file

@ -19,6 +19,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -32,6 +33,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.foundation.shape.RoundedCornerShape
import com.labfytools.trainlog.R import com.labfytools.trainlog.R
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -105,7 +107,16 @@ fun AndroidAppShell(
onOpenSection(section) onOpenSection(section)
scope.launch { drawerState.close() } scope.launch { drawerState.close() }
}, },
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
shape = RoundedCornerShape(10.dp),
colors = NavigationDrawerItemDefaults.colors(
selectedContainerColor = colors.surfaceAlt,
selectedIconColor = colors.accent,
selectedTextColor = colors.text,
unselectedContainerColor = colors.mantle,
unselectedIconColor = colors.muted,
unselectedTextColor = colors.muted,
),
) )
} }
} }

View file

@ -8,6 +8,12 @@ package com.labfytools.trainlog.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -89,148 +95,74 @@ fun BodyScreen(
TrainlogFrame( TrainlogFrame(
title = strings.getString(R.string.general) title = strings.getString(R.string.general)
) { ) {
BodyMetricField( BodyMetricGrid(
label = strings.getString(R.string.weight), listOf(
unit = "kg", BodyMetricItem(strings.getString(R.string.weight), "kg", bodyWeight) {
value = bodyWeight, bodyWeight = it
onValueChange = { message = null
bodyWeight = it },
message = null BodyMetricItem(strings.getString(R.string.neck), "cm", neck) {
}, neck = it
) message = null
},
BodyMetricField( BodyMetricItem(strings.getString(R.string.shoulders), "cm", shoulders) {
label = strings.getString(R.string.neck), shoulders = it
unit = "cm", message = null
value = neck, },
onValueChange = { BodyMetricItem(strings.getString(R.string.chest), "cm", chest) {
neck = it chest = it
message = null message = null
}, },
) BodyMetricItem(strings.getString(R.string.waist), "cm", waist) {
waist = it
BodyMetricField( message = null
label = strings.getString(R.string.shoulders), },
unit = "cm", BodyMetricItem(strings.getString(R.string.hips), "cm", hips) {
value = shoulders, hips = it
onValueChange = { message = null
shoulders = it },
message = null ),
},
)
BodyMetricField(
label = strings.getString(R.string.chest),
unit = "cm",
value = chest,
onValueChange = {
chest = it
message = null
},
)
BodyMetricField(
label = strings.getString(R.string.waist),
unit = "cm",
value = waist,
onValueChange = {
waist = it
message = null
},
)
BodyMetricField(
label = strings.getString(R.string.hips),
unit = "cm",
value = hips,
onValueChange = {
hips = it
message = null
},
) )
} }
TrainlogFrame( TrainlogFrame(
title = strings.getString(R.string.limbs) title = strings.getString(R.string.limbs)
) { ) {
BodyMetricField( BodyMetricGrid(
label = strings.getString(R.string.left_arm), listOf(
unit = "cm", BodyMetricItem(strings.getString(R.string.left_arm), "cm", leftArm) {
value = leftArm, leftArm = it
onValueChange = { message = null
leftArm = it },
message = null BodyMetricItem(strings.getString(R.string.right_arm), "cm", rightArm) {
}, rightArm = it
) message = null
},
BodyMetricField( BodyMetricItem(strings.getString(R.string.left_forearm), "cm", leftForearm) {
label = strings.getString(R.string.right_arm), leftForearm = it
unit = "cm", message = null
value = rightArm, },
onValueChange = { BodyMetricItem(strings.getString(R.string.right_forearm), "cm", rightForearm) {
rightArm = it rightForearm = it
message = null message = null
}, },
) BodyMetricItem(strings.getString(R.string.left_thigh), "cm", leftThigh) {
leftThigh = it
BodyMetricField( message = null
label = strings.getString(R.string.left_forearm), },
unit = "cm", BodyMetricItem(strings.getString(R.string.right_thigh), "cm", rightThigh) {
value = leftForearm, rightThigh = it
onValueChange = { message = null
leftForearm = it },
message = null BodyMetricItem(strings.getString(R.string.left_calf), "cm", leftCalf) {
}, leftCalf = it
) message = null
},
BodyMetricField( BodyMetricItem(strings.getString(R.string.right_calf), "cm", rightCalf) {
label = strings.getString(R.string.right_forearm), rightCalf = it
unit = "cm", message = null
value = rightForearm, },
onValueChange = { ),
rightForearm = it
message = null
},
)
BodyMetricField(
label = strings.getString(R.string.left_thigh),
unit = "cm",
value = leftThigh,
onValueChange = {
leftThigh = it
message = null
},
)
BodyMetricField(
label = strings.getString(R.string.right_thigh),
unit = "cm",
value = rightThigh,
onValueChange = {
rightThigh = it
message = null
},
)
BodyMetricField(
label = strings.getString(R.string.left_calf),
unit = "cm",
value = leftCalf,
onValueChange = {
leftCalf = it
message = null
},
)
BodyMetricField(
label = strings.getString(R.string.right_calf),
unit = "cm",
value = rightCalf,
onValueChange = {
rightCalf = it
message = null
},
) )
} }
@ -426,12 +358,14 @@ private fun BodyMetricField(
unit: String, unit: String,
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
TrainlogInputField( TrainlogInputField(
label = "$label ($unit)", label = label,
value = value, value = value,
onValueChange = onValueChange =
onValueChange, onValueChange,
modifier = modifier,
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
keyboardType = keyboardType =
@ -439,9 +373,42 @@ private fun BodyMetricField(
imeAction = imeAction =
ImeAction.Next, ImeAction.Next,
), ),
suffix = unit,
compact = true,
) )
} }
private data class BodyMetricItem(
val label: String,
val unit: String,
val value: String,
val onValueChange: (String) -> Unit,
)
/** Two columns retain scanability; very narrow accessibility layouts stack. */
@Composable
private fun ColumnScope.BodyMetricGrid(items: List<BodyMetricItem>) {
BoxWithConstraints(Modifier.fillMaxWidth()) {
val columns = if (maxWidth < 330.dp) 1 else 2
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
items.chunked(columns).forEach { rowItems ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
rowItems.forEach { item ->
BodyMetricField(
item.label,
item.unit,
item.value,
item.onValueChange,
Modifier.weight(1f),
)
}
repeat(columns - rowItems.size) { androidx.compose.foundation.layout.Spacer(Modifier.weight(1f)) }
}
}
}
}
}
private sealed interface MetricParse { private sealed interface MetricParse {
data object Empty : data object Empty :
MetricParse MetricParse

View file

@ -12,6 +12,7 @@ import com.labfytools.trainlog.model.SessionType
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
import com.labfytools.trainlog.R import com.labfytools.trainlog.R
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@Composable @Composable
@ -133,7 +134,8 @@ internal fun formatStartedAt(
internal fun formatDate(value: String): String { internal fun formatDate(value: String): String {
val locale = presentationLocale() val locale = presentationLocale()
val pattern = uiString(R.string.date_pattern) val pattern = uiString(R.string.date_pattern)
return runCatching { val formatter = DateTimeFormatter.ofPattern(pattern, locale)
OffsetDateTime.parse(value).format(DateTimeFormatter.ofPattern(pattern, locale)) return runCatching { LocalDate.parse(value.take(10)).format(formatter) }
}.getOrElse { value.take(10) } .recoverCatching { OffsetDateTime.parse(value).format(formatter) }
.getOrElse { value.take(10) }
} }

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -21,7 +22,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -38,6 +41,8 @@ import com.labfytools.trainlog.data.AndroidBackupService
import com.labfytools.trainlog.data.AndroidBackupResult import com.labfytools.trainlog.data.AndroidBackupResult
import com.labfytools.trainlog.data.directStoragePermissionIntent import com.labfytools.trainlog.data.directStoragePermissionIntent
import com.labfytools.trainlog.model.ActiveSessionDraft import com.labfytools.trainlog.model.ActiveSessionDraft
import com.labfytools.trainlog.model.SessionExerciseDraft
import com.labfytools.trainlog.ui.theme.TrainlogTypography
import com.labfytools.trainlog.ui.theme.LocalTrainlogColors import com.labfytools.trainlog.ui.theme.LocalTrainlogColors
import com.labfytools.trainlog.R import com.labfytools.trainlog.R
@ -95,21 +100,14 @@ fun AiSessionDraftsScreen(
message?.let { TrainlogInfo(it) } message?.let { TrainlogInfo(it) }
if (drafts.isEmpty() && preparations.isEmpty()) TrainlogInfo(strings.getString(R.string.draft_none)) if (drafts.isEmpty() && preparations.isEmpty()) TrainlogInfo(strings.getString(R.string.draft_none))
preparations.forEach { preparation -> preparations.forEach { preparation ->
TrainlogFrame(preparation.title, active = true) { DraftSummaryCard(
TrainlogInfo(listOfNotNull( title = preparation.title,
strings.getString(R.string.manual_preparation), provenance = strings.getString(R.string.manual_preparation),
preparation.plannedFor?.let { strings.getString(R.string.planned_for, formatDate(it)) }, plannedFor = preparation.plannedFor,
strings.resources.getQuantityString(R.plurals.exercise_count, preparation.entries.size, preparation.entries.size), notes = preparation.notes,
).joinToString(" · ")) entries = preparation.entries,
preparation.notes?.let { TrainlogInfo(it) } startDescription = strings.getString(R.string.start_preparation_description),
preparation.entries.forEach { entry -> onStart = {
val plan = checkNotNull(entry.plan)
val metric = plan.reps?.let { strings.resources.getQuantityString(R.plurals.repetition_count, it, it) }
?: plan.durationSeconds?.let { strings.resources.getQuantityString(R.plurals.seconds_count, it, it) }.orEmpty()
val weight = plan.weightKg?.let { " · $it kg" }.orEmpty()
TrainlogInfo(strings.getString(R.string.plan_draft_summary, entry.exercise.name, plan.sets, metric, weight, plan.restSeconds))
}
TrainlogPrimaryAction(strings.getString(R.string.start), strings.getString(R.string.start_preparation_description)) {
when (val result = repository.startPreparedSession(preparation.deliveryId)) { when (val result = repository.startPreparedSession(preparation.deliveryId)) {
StartAiSessionDraftResult.Started -> { onPendingChanged(); onStarted() } StartAiSessionDraftResult.Started -> { onPendingChanged(); onStarted() }
StartAiSessionDraftResult.ExistingActiveDraft -> StartAiSessionDraftResult.ExistingActiveDraft ->
@ -117,24 +115,18 @@ fun AiSessionDraftsScreen(
StartAiSessionDraftResult.NotPending -> { message = strings.getString(R.string.draft_missing); revision++; onPendingChanged() } StartAiSessionDraftResult.NotPending -> { message = strings.getString(R.string.draft_missing); revision++; onPendingChanged() }
is StartAiSessionDraftResult.Error -> message = localizedRepositoryMessage(strings, result.message) is StartAiSessionDraftResult.Error -> message = localizedRepositoryMessage(strings, result.message)
} }
} },
} )
} }
drafts.forEach { draft -> drafts.forEach { draft ->
TrainlogFrame(draft.title ?: strings.getString(R.string.session_proposal), active = true) { DraftSummaryCard(
TrainlogInfo(listOfNotNull( title = draft.title ?: strings.getString(R.string.session_proposal),
draft.plannedFor?.let { strings.getString(R.string.planned_for, formatDate(it)) }, provenance = strings.getString(R.string.session_proposal),
strings.resources.getQuantityString(R.plurals.exercise_count, draft.entries.size, draft.entries.size), plannedFor = draft.plannedFor,
).joinToString(" · ")) notes = draft.notes,
draft.notes?.let { TrainlogInfo(it) } entries = draft.entries,
draft.entries.forEach { entry -> startDescription = strings.getString(R.string.start_proposal_description),
val plan = checkNotNull(entry.plan) onStart = {
val metric = plan.reps?.let { strings.resources.getQuantityString(R.plurals.repetition_count, it, it) }
?: plan.durationSeconds?.let { strings.resources.getQuantityString(R.plurals.seconds_count, it, it) }.orEmpty()
val weight = plan.weightKg?.let { " · $it kg" }.orEmpty()
TrainlogInfo(strings.getString(R.string.plan_draft_summary, entry.exercise.name, plan.sets, metric, weight, plan.restSeconds))
}
TrainlogPrimaryAction(strings.getString(R.string.start), strings.getString(R.string.start_proposal_description)) {
when (val result = repository.startAiSessionDraft(draft.draftId)) { when (val result = repository.startAiSessionDraft(draft.draftId)) {
StartAiSessionDraftResult.Started -> { StartAiSessionDraftResult.Started -> {
onPendingChanged() onPendingChanged()
@ -149,15 +141,24 @@ fun AiSessionDraftsScreen(
} }
is StartAiSessionDraftResult.Error -> message = localizedRepositoryMessage(strings, result.message) is StartAiSessionDraftResult.Error -> message = localizedRepositoryMessage(strings, result.message)
} }
} },
TrainlogDeleteButton( footer = {
contentDescription = localizedContext().getString(R.string.a11y_delete_draft), Row(
modifier = Modifier.testTag("delete-ai-draft-${draft.draftId}"), Modifier.fillMaxWidth(),
onClick = { horizontalArrangement = Arrangement.End,
pendingDeletion = draft.draftId to (draft.title ?: strings.getString(R.string.session_proposal)) ) {
}, TrainlogDeleteButton(
) contentDescription = localizedContext().getString(R.string.a11y_delete_draft),
} modifier = Modifier.testTag("delete-ai-draft-${draft.draftId}"),
fullWidth = false,
onClick = {
pendingDeletion = draft.draftId to
(draft.title ?: strings.getString(R.string.session_proposal))
},
)
}
},
)
} }
} }
pendingDeletion?.let { (draftId, title) -> pendingDeletion?.let { (draftId, title) ->
@ -183,6 +184,138 @@ fun AiSessionDraftsScreen(
} }
} }
/** Compact presentation of an inert preparation; starting remains caller-owned. */
@Composable
private fun DraftSummaryCard(
title: String,
provenance: String,
plannedFor: String?,
notes: String?,
entries: List<SessionExerciseDraft>,
startDescription: String,
onStart: () -> Unit,
footer: (@Composable () -> Unit)? = null,
) {
val colors = LocalTrainlogColors.current
val strings = localizedContext()
Column(Modifier.fillMaxWidth().padding(bottom = 12.dp)) {
TrainlogCompactCard {
androidx.compose.material3.Text(
title,
color = colors.text,
style = TrainlogTypography.section,
fontWeight = FontWeight.Bold,
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
TrainlogMetaBadge(provenance, Modifier.weight(1f))
TrainlogMetaBadge(
strings.resources.getQuantityString(
R.plurals.exercise_count,
entries.size,
entries.size,
),
)
}
plannedFor?.let {
androidx.compose.material3.Text(
strings.getString(R.string.planned_for, formatDate(it)),
color = colors.muted,
style = TrainlogTypography.small,
)
}
notes?.takeIf { it.isNotBlank() }?.let {
androidx.compose.material3.Text(
it,
color = colors.muted,
style = TrainlogTypography.small,
maxLines = 2,
)
}
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
entries.forEach { DraftExerciseRow(it) }
}
Row(
Modifier.fillMaxWidth().padding(top = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TrainlogButton(
strings.getString(R.string.start),
onStart,
Modifier.widthIn(min = 128.dp),
style = TrainlogButtonStyle.SUCCESS,
)
androidx.compose.material3.Text(
startDescription,
modifier = Modifier.weight(1f),
color = colors.muted,
style = TrainlogTypography.small,
maxLines = 2,
)
}
footer?.invoke()
}
}
}
@Composable
private fun DraftExerciseRow(entry: SessionExerciseDraft) {
val colors = LocalTrainlogColors.current
val plan = checkNotNull(entry.plan)
Column(
Modifier.fillMaxWidth()
.background(colors.background, androidx.compose.material3.MaterialTheme.shapes.small)
.padding(horizontal = 10.dp, vertical = 8.dp),
) {
androidx.compose.material3.Text(
entry.exercise.name,
color = colors.text,
style = TrainlogTypography.small,
fontWeight = FontWeight.SemiBold,
)
androidx.compose.material3.Text(
draftPlanSummary(plan.sets, plan.reps, plan.durationSeconds, plan.weightKg, plan.restSeconds),
color = colors.muted,
style = TrainlogTypography.small,
)
}
}
@Composable
private fun draftPlanSummary(
sets: Int,
reps: Int?,
durationSeconds: Int?,
weightKg: Double?,
restSeconds: Int,
): String {
val strings = localizedContext()
val metric = when {
reps != null -> "$sets × $reps"
durationSeconds != null && sets > 0 -> "$sets × ${humanDuration(durationSeconds)}"
durationSeconds != null -> humanDuration(durationSeconds)
else -> "—"
}
return buildList {
add(metric)
weightKg?.let { add("${it.toString().trimEnd('0').trimEnd('.')} kg") }
if (restSeconds > 0) add(strings.getString(R.string.draft_rest_short, restSeconds))
}.joinToString(" · ")
}
@Composable
private fun humanDuration(seconds: Int): String {
val strings = localizedContext()
return if (seconds % 60 == 0) {
val minutes = seconds / 60
strings.resources.getQuantityString(R.plurals.duration_minutes_value, minutes, minutes)
} else {
strings.resources.getQuantityString(R.plurals.duration_seconds_value, seconds, seconds)
}
}
@Composable @Composable
fun LatestMaximaScreen(repository: TrainlogRepository) { fun LatestMaximaScreen(repository: TrainlogRepository) {
val colors = LocalTrainlogColors.current val colors = LocalTrainlogColors.current

View file

@ -201,6 +201,44 @@ fun TrainlogAction(
} }
} }
/** Presentation-only surface for dense object summaries and repeated rows. */
@Composable
fun TrainlogCompactCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
val colors = LocalTrainlogColors.current
val interaction = if (onClick == null) Modifier else Modifier.clickable(onClick = onClick)
Column(
modifier
.fillMaxWidth()
.background(colors.surface, MaterialTheme.shapes.medium)
.then(interaction)
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
content = content,
)
}
/** Small non-interactive metadata token; business state remains caller-owned. */
@Composable
fun TrainlogMetaBadge(
text: String,
modifier: Modifier = Modifier,
accent: Color? = null,
) {
val colors = LocalTrainlogColors.current
Text(
text = text,
modifier = modifier.background(colors.surfaceAlt, MaterialTheme.shapes.small)
.padding(horizontal = 8.dp, vertical = 4.dp),
color = accent ?: colors.text,
style = TrainlogTypography.small,
maxLines = 1,
)
}
enum class TrainlogButtonStyle { PRIMARY, SECONDARY, SUCCESS, DESTRUCTIVE, GHOST } enum class TrainlogButtonStyle { PRIMARY, SECONDARY, SUCCESS, DESTRUCTIVE, GHOST }
/** Compact pictogram action with a mandatory accessible name. */ /** Compact pictogram action with a mandatory accessible name. */
@ -229,11 +267,12 @@ fun TrainlogDeleteButton(
contentDescription: String, contentDescription: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
fullWidth: Boolean = true,
) { ) {
val colors = LocalTrainlogColors.current val colors = LocalTrainlogColors.current
Button( Button(
onClick = onClick, onClick = onClick,
modifier = modifier.fillMaxWidth().heightIn(min = 52.dp), modifier = (if (fullWidth) modifier.fillMaxWidth() else modifier).heightIn(min = 48.dp),
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(containerColor = colors.error), colors = ButtonDefaults.buttonColors(containerColor = colors.error),
) { ) {
@ -364,6 +403,8 @@ fun TrainlogInputField(
singleLine: Boolean = true, singleLine: Boolean = true,
minLines: Int = 1, minLines: Int = 1,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
suffix: String? = null,
compact: Boolean = false,
) { ) {
val colors = val colors =
LocalTrainlogColors.current LocalTrainlogColors.current
@ -418,7 +459,7 @@ fun TrainlogInputField(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 48.dp) .heightIn(min = if (compact) 44.dp else 48.dp)
.onFocusChanged { .onFocusChanged {
focused = it.isFocused focused = it.isFocused
} }
@ -429,11 +470,22 @@ fun TrainlogInputField(
Modifier.testTag(testTag) Modifier.testTag(testTag)
} }
) )
.background(colors.surface) .background(colors.surface, MaterialTheme.shapes.medium)
.padding( .padding(
horizontal = 12.dp, horizontal = if (compact) 10.dp else 12.dp,
vertical = 10.dp, vertical = if (compact) 8.dp else 10.dp,
), ),
decorationBox = { innerTextField ->
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Box(Modifier.weight(1f)) { innerTextField() }
suffix?.let {
BasicText(it, style = TrainlogTypography.small.copy(color = colors.muted))
}
}
},
) )
Box( Box(

View file

@ -334,7 +334,8 @@
<plurals name="repetition_count"><item quantity="one">%d repetition</item><item quantity="other">%d repetitions</item></plurals> <plurals name="repetition_count"><item quantity="one">%d repetition</item><item quantity="other">%d repetitions</item></plurals>
<string name="start">Start</string><string name="start_proposal_description">Copy this proposal into the current session.</string> <string name="start">Start</string><string name="start_proposal_description">Copy this proposal into the current session.</string>
<string name="manual_preparation">Manual preparation</string> <string name="manual_preparation">Manual preparation</string>
<string name="start_preparation_description">Start this preparation without replacing an active session.</string> <string name="start_preparation_description">Creates a protected active session.</string>
<string name="draft_rest_short">rest %1$d s</string>
<string name="draft_already_active">A session is already in progress. The imported draft was preserved.</string> <string name="draft_already_active">A session is already in progress. The imported draft was preserved.</string>
<string name="delete_draft_detail">Draft “%1$s” will be deleted.\nThis action cannot be undone.</string> <string name="delete_draft_detail">Draft “%1$s” will be deleted.\nThis action cannot be undone.</string>
<string name="training">Training</string><string name="resume_current_session">Resume current session</string> <string name="training">Training</string><string name="resume_current_session">Resume current session</string>

View file

@ -334,7 +334,8 @@
<plurals name="repetition_count"><item quantity="one">%d répétition</item><item quantity="other">%d répétitions</item></plurals> <plurals name="repetition_count"><item quantity="one">%d répétition</item><item quantity="other">%d répétitions</item></plurals>
<string name="start">Démarrer</string><string name="start_proposal_description">Copier cette proposition dans la séance en cours.</string> <string name="start">Démarrer</string><string name="start_proposal_description">Copier cette proposition dans la séance en cours.</string>
<string name="manual_preparation">Préparation manuelle</string> <string name="manual_preparation">Préparation manuelle</string>
<string name="start_preparation_description">Démarrer cette préparation sans écraser une séance en cours.</string> <string name="start_preparation_description">Crée une séance en cours protégée.</string>
<string name="draft_rest_short">repos %1$d s</string>
<string name="draft_already_active">Une séance est déjà en cours. Le brouillon importé a été conservé.</string> <string name="draft_already_active">Une séance est déjà en cours. Le brouillon importé a été conservé.</string>
<string name="delete_draft_detail">Le brouillon « %1$s » sera supprimé.\nCette action est irréversible.</string> <string name="delete_draft_detail">Le brouillon « %1$s » sera supprimé.\nCette action est irréversible.</string>
<string name="training">Entraînement</string><string name="resume_current_session">Reprendre la séance en cours</string> <string name="training">Entraînement</string><string name="resume_current_session">Reprendre la séance en cours</string>

View file

@ -139,8 +139,8 @@ class AiSessionDraftUiWiringTest {
} } } }
compose.onNodeWithText("Dos + biceps").assertIsDisplayed() compose.onNodeWithText("Dos + biceps").assertIsDisplayed()
compose.onNodeWithText("Tractions supination · 3 × 10 répétitions · repos 90 s") compose.onNodeWithText("Tractions supination").assertIsDisplayed()
.assertIsDisplayed() compose.onNodeWithText("3 × 10 · repos 90 s").assertIsDisplayed()
compose.onNodeWithText("Accent sur le dos et les biceps.").assertIsDisplayed() compose.onNodeWithText("Accent sur le dos et les biceps.").assertIsDisplayed()
} }