Polish editors, previews, persistence and docs

Cross-platform refinements to appearance/battery/charger editors, tabs
and navigation, plus persistence, screenshot previews and CLAUDE.md docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 01:04:47 +02:00
parent d97e3a2b7c
commit 23b117bfe2
16 changed files with 287 additions and 89 deletions

View File

@@ -2,6 +2,15 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Two apps in this repo
- **iOS** (`Cable/`) — SwiftUI + SwiftData. **This document describes the iOS app unless stated otherwise.**
- **Android** (`android/`) — a native Kotlin/Jetpack Compose port that mirrors every iOS feature (package root `app.voltplan.cable`, Room persistence, same Aptabase analytics). See **`android/README.md`** for its architecture and build instructions before working on it.
Behavior and data shape are meant to stay in sync across both — when changing a user-facing feature on one platform, check the other. See [Export Options](#export-options) for one such cross-platform contract.
**Apply every instruction to both the iOS and Android versions unless explicitly told otherwise.** Any feature, fix, or change requested without naming a platform must land on both apps and stay behaviorally in sync.
## Build & Test Commands ## Build & Test Commands
Use the **Xcode MCP server** tools instead of `xcodebuild` CLI: Use the **Xcode MCP server** tools instead of `xcodebuild` CLI:
@@ -106,13 +115,25 @@ All list views use consistent styling:
- Use `String(localized:defaultValue:)`**not** `NSLocalizedString`. The `defaultValue` serves as English fallback and avoids showing raw keys when a Localizable.strings entry is missing. - Use `String(localized:defaultValue:)`**not** `NSLocalizedString`. The `defaultValue` serves as English fallback and avoids showing raw keys when a Localizable.strings entry is missing.
- When adding new user-facing strings, add translations to **all 5** Localizable.strings files immediately. - When adding new user-facing strings, add translations to **all 5** Localizable.strings files immediately.
## PDF Export Pattern ## Export Options
Three export options, available from the Overview tab's share menu plus the BOM sheet. **Keep iOS and Android (`android/.../pdf/`) in sync — they must offer the same exports.**
1. **System Overview (PDF)** — summary + a full-page wiring diagram + per-entity tables.
2. **Bill of Materials (PDF)** — categorized component list.
3. **Wiring Diagram (PNG)** — standalone diagram image.
The wiring diagram (used both as the standalone PNG and the Overview PDF's diagram page) is fetched from the shared **VoltPlan diagram API** (`POST https://voltplan.app/api/diagram/generate`, JSON payload of system/loads/batteries/chargers, returns PNG). Both platforms send the identical payload shape; falls back gracefully when the API is unreachable (iOS draws a Core Graphics diagram; Android omits the PDF page / shows an error toast for the standalone export).
### PDF Export Pattern (iOS)
PDF exports use `UIGraphicsPDFRenderer` with A4 portrait format. The pattern is: PDF exports use `UIGraphicsPDFRenderer` with A4 portrait format. The pattern is:
- **Exporter struct** (e.g. `SystemOverviewPDFExporter`, `SystemBillOfMaterialsPDFExporter`) with snapshot data types — keeps Core Graphics rendering isolated from SwiftUI/SwiftData. - **Exporter struct** (e.g. `SystemOverviewPDFExporter`, `SystemBillOfMaterialsPDFExporter`) with snapshot data types — keeps Core Graphics rendering isolated from SwiftUI/SwiftData.
- **ShareSheet** triggered via `@State` item binding in the parent view. - **ShareSheet** triggered via `@State` item binding in the parent view.
- **Toolbar button** (not inline content) for the export action. - **Toolbar button** (not inline content) for the export action.
On Android the equivalents live in `android/app/src/main/java/app/voltplan/cable/pdf/`: `SystemOverviewPdf`, `SystemBomPdf`, `SystemDiagram` (diagram fetch + PNG export), and `PdfShare` (`PdfDocument` + `FileProvider`/`ACTION_SEND`).
## Screenshots ## Screenshots
App Store screenshots are generated via XCUITests running on simulators, automated by `shooter.sh`. App Store screenshots are generated via XCUITests running on simulators, automated by `shooter.sh`.

View File

@@ -44,7 +44,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
var iconName: String var iconName: String
var colorName: String var colorName: String
var system: ElectricalSystem var system: ElectricalSystem
var componentID: String?
init( init(
id: UUID = UUID(), id: UUID = UUID(),
name: String, name: String,
@@ -58,7 +59,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
maximumTemperatureCelsius: Double = 60, maximumTemperatureCelsius: Double = 60,
iconName: String = "battery.100", iconName: String = "battery.100",
colorName: String = "blue", colorName: String = "blue",
system: ElectricalSystem system: ElectricalSystem,
componentID: String? = nil
) { ) {
self.id = id self.id = id
self.name = name self.name = name
@@ -73,6 +75,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.iconName = iconName self.iconName = iconName
self.colorName = colorName self.colorName = colorName
self.system = system self.system = system
self.componentID = componentID
} }
init(savedBattery: SavedBattery, system: ElectricalSystem) { init(savedBattery: SavedBattery, system: ElectricalSystem) {
@@ -95,6 +98,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.iconName = savedBattery.iconName self.iconName = savedBattery.iconName
self.colorName = savedBattery.colorName self.colorName = savedBattery.colorName
self.system = system self.system = system
self.componentID = savedBattery.componentID
} }
var energyWattHours: Double { var energyWattHours: Double {
@@ -137,6 +141,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
savedBattery.iconName = iconName savedBattery.iconName = iconName
savedBattery.colorName = colorName savedBattery.colorName = colorName
savedBattery.system = system savedBattery.system = system
savedBattery.componentID = componentID
savedBattery.timestamp = Date() savedBattery.timestamp = Date()
} }
} }
@@ -154,7 +159,8 @@ extension BatteryConfiguration {
lhs.maximumTemperatureCelsius == rhs.maximumTemperatureCelsius && lhs.maximumTemperatureCelsius == rhs.maximumTemperatureCelsius &&
lhs.chemistry == rhs.chemistry && lhs.chemistry == rhs.chemistry &&
lhs.iconName == rhs.iconName && lhs.iconName == rhs.iconName &&
lhs.colorName == rhs.colorName lhs.colorName == rhs.colorName &&
lhs.componentID == rhs.componentID
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
@@ -170,5 +176,6 @@ extension BatteryConfiguration {
hasher.combine(chemistry) hasher.combine(chemistry)
hasher.combine(iconName) hasher.combine(iconName)
hasher.combine(colorName) hasher.combine(colorName)
hasher.combine(componentID)
} }
} }

View File

@@ -12,6 +12,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
var colorName: String var colorName: String
var system: ElectricalSystem var system: ElectricalSystem
var powerSourceType: SavedCharger.PowerSourceType var powerSourceType: SavedCharger.PowerSourceType
var componentID: String?
var remoteIconURLString: String?
init( init(
id: UUID = UUID(), id: UUID = UUID(),
@@ -23,7 +25,9 @@ struct ChargerConfiguration: Identifiable, Hashable {
iconName: String = "bolt.fill", iconName: String = "bolt.fill",
colorName: String = "orange", colorName: String = "orange",
system: ElectricalSystem, system: ElectricalSystem,
powerSourceType: SavedCharger.PowerSourceType = .shore powerSourceType: SavedCharger.PowerSourceType = .shore,
componentID: String? = nil,
remoteIconURLString: String? = nil
) { ) {
self.id = id self.id = id
self.name = name self.name = name
@@ -35,6 +39,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
self.colorName = colorName self.colorName = colorName
self.system = system self.system = system
self.powerSourceType = powerSourceType self.powerSourceType = powerSourceType
self.componentID = componentID
self.remoteIconURLString = remoteIconURLString
} }
init(savedCharger: SavedCharger, system: ElectricalSystem) { init(savedCharger: SavedCharger, system: ElectricalSystem) {
@@ -48,6 +54,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
self.colorName = savedCharger.colorName self.colorName = savedCharger.colorName
self.system = system self.system = system
self.powerSourceType = savedCharger.sourceType self.powerSourceType = savedCharger.sourceType
self.componentID = savedCharger.componentID
self.remoteIconURLString = savedCharger.remoteIconURLString
} }
var effectivePowerWatts: Double { var effectivePowerWatts: Double {
@@ -67,6 +75,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
savedCharger.colorName = colorName savedCharger.colorName = colorName
savedCharger.system = system savedCharger.system = system
savedCharger.powerSourceType = powerSourceType.rawValue savedCharger.powerSourceType = powerSourceType.rawValue
savedCharger.componentID = componentID
savedCharger.remoteIconURLString = remoteIconURLString
savedCharger.timestamp = Date() savedCharger.timestamp = Date()
} }
} }

View File

@@ -7,6 +7,7 @@ struct ChargersView: View {
let onAdd: () -> Void let onAdd: () -> Void
let onEdit: (SavedCharger) -> Void let onEdit: (SavedCharger) -> Void
let onDelete: (IndexSet) -> Void let onDelete: (IndexSet) -> Void
let onBrowseLibrary: () -> Void
private struct SummaryMetric: Identifiable { private struct SummaryMetric: Identifiable {
let id: String let id: String
@@ -94,13 +95,15 @@ struct ChargersView: View {
editMode: Binding<EditMode> = .constant(.inactive), editMode: Binding<EditMode> = .constant(.inactive),
onAdd: @escaping () -> Void = {}, onAdd: @escaping () -> Void = {},
onEdit: @escaping (SavedCharger) -> Void = { _ in }, onEdit: @escaping (SavedCharger) -> Void = { _ in },
onDelete: @escaping (IndexSet) -> Void = { _ in } onDelete: @escaping (IndexSet) -> Void = { _ in },
onBrowseLibrary: @escaping () -> Void = {}
) { ) {
self.system = system self.system = system
self.chargers = chargers self.chargers = chargers
self.onAdd = onAdd self.onAdd = onAdd
self.onEdit = onEdit self.onEdit = onEdit
self.onDelete = onDelete self.onDelete = onDelete
self.onBrowseLibrary = onBrowseLibrary
_editMode = editMode _editMode = editMode
} }
@@ -244,7 +247,8 @@ struct ChargersView: View {
private var emptyState: some View { private var emptyState: some View {
OnboardingInfoView( OnboardingInfoView(
configuration: .charger(), configuration: .charger(),
onPrimaryAction: onAdd onPrimaryAction: onAdd,
onSecondaryAction: onBrowseLibrary
) )
.padding(.horizontal, 0) .padding(.horizontal, 0)
} }

View File

@@ -221,56 +221,56 @@ private struct ComponentLibraryScreenshot: View {
translations: ["de": "Navigationslichter", "es": "Luces de navegación", "fr": "Feux de navigation", "nl": "Navigatieverlichting"], translations: ["de": "Navigationslichter", "es": "Luces de navegación", "fr": "Feux de navigation", "nl": "Navigatieverlichting"],
voltageIn: 12.8, voltageOut: nil, watt: 25, voltageIn: 12.8, voltageOut: nil, watt: 25,
dutyCyclePercent: 100, defaultUtilizationFactorPercent: 40, dutyCyclePercent: 100, defaultUtilizationFactorPercent: 40,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "2", name: "Refrigerator Compressor", id: "2", name: "Refrigerator Compressor",
translations: ["de": "Kühlschrank-Kompressor", "es": "Compresor de refrigerador", "fr": "Compresseur réfrigérateur", "nl": "Koelkastcompressor"], translations: ["de": "Kühlschrank-Kompressor", "es": "Compresor de refrigerador", "fr": "Compresseur réfrigérateur", "nl": "Koelkastcompressor"],
voltageIn: 12.8, voltageOut: nil, watt: 48, voltageIn: 12.8, voltageOut: nil, watt: 48,
dutyCyclePercent: 40, defaultUtilizationFactorPercent: 100, dutyCyclePercent: 40, defaultUtilizationFactorPercent: 100,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "3", name: "Anchor Windlass", id: "3", name: "Anchor Windlass",
translations: ["de": "Ankerwinde", "es": "Molinete de ancla", "fr": "Guindeau", "nl": "Ankerlier"], translations: ["de": "Ankerwinde", "es": "Molinete de ancla", "fr": "Guindeau", "nl": "Ankerlier"],
voltageIn: 12.8, voltageOut: nil, watt: 960, voltageIn: 12.8, voltageOut: nil, watt: 960,
dutyCyclePercent: 5, defaultUtilizationFactorPercent: 2, dutyCyclePercent: 5, defaultUtilizationFactorPercent: 2,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "4", name: "VHF Radio", id: "4", name: "VHF Radio",
translations: ["de": "UKW Funkgerät", "es": "Radio VHF", "fr": "Radio VHF", "nl": "Marifoon"], translations: ["de": "UKW Funkgerät", "es": "Radio VHF", "fr": "Radio VHF", "nl": "Marifoon"],
voltageIn: 12.8, voltageOut: nil, watt: 72, voltageIn: 12.8, voltageOut: nil, watt: 72,
dutyCyclePercent: 30, defaultUtilizationFactorPercent: 33, dutyCyclePercent: 30, defaultUtilizationFactorPercent: 33,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "5", name: "LED Interior Lights", id: "5", name: "LED Interior Lights",
translations: ["de": "LED Innenbeleuchtung", "es": "Iluminación LED interior", "fr": "Éclairage LED intérieur", "nl": "LED binnenverlichting"], translations: ["de": "LED Innenbeleuchtung", "es": "Iluminación LED interior", "fr": "Éclairage LED intérieur", "nl": "LED binnenverlichting"],
voltageIn: 12.8, voltageOut: nil, watt: 18, voltageIn: 12.8, voltageOut: nil, watt: 18,
dutyCyclePercent: 100, defaultUtilizationFactorPercent: 25, dutyCyclePercent: 100, defaultUtilizationFactorPercent: 25,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "6", name: "Water Pump", id: "6", name: "Water Pump",
translations: ["de": "Wasserpumpe", "es": "Bomba de agua", "fr": "Pompe à eau", "nl": "Waterpomp"], translations: ["de": "Wasserpumpe", "es": "Bomba de agua", "fr": "Pompe à eau", "nl": "Waterpomp"],
voltageIn: 12.8, voltageOut: nil, watt: 42, voltageIn: 12.8, voltageOut: nil, watt: 42,
dutyCyclePercent: 20, defaultUtilizationFactorPercent: 10, dutyCyclePercent: 20, defaultUtilizationFactorPercent: 10,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "7", name: "Diesel Heater", id: "7", name: "Diesel Heater",
translations: ["de": "Dieselheizung", "es": "Calefactor diésel", "fr": "Chauffage diesel", "nl": "Dieselverwarming"], translations: ["de": "Dieselheizung", "es": "Calefactor diésel", "fr": "Chauffage diesel", "nl": "Dieselverwarming"],
voltageIn: 12.8, voltageOut: nil, watt: 36, voltageIn: 12.8, voltageOut: nil, watt: 36,
dutyCyclePercent: 60, defaultUtilizationFactorPercent: 50, dutyCyclePercent: 60, defaultUtilizationFactorPercent: 50,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
ComponentLibraryItem( ComponentLibraryItem(
id: "8", name: "USB Charging Station", id: "8", name: "USB Charging Station",
translations: ["de": "USB-Ladestation", "es": "Estación de carga USB", "fr": "Station de charge USB", "nl": "USB-laadstation"], translations: ["de": "USB-Ladestation", "es": "Estación de carga USB", "fr": "Station de charge USB", "nl": "USB-laadstation"],
voltageIn: 12.8, voltageOut: nil, watt: 24, voltageIn: 12.8, voltageOut: nil, watt: 24,
dutyCyclePercent: 100, defaultUtilizationFactorPercent: 30, dutyCyclePercent: 100, defaultUtilizationFactorPercent: 30,
iconURL: nil componentCategory: nil, iconURL: nil
), ),
] ]
} }

View File

@@ -114,6 +114,87 @@ struct SystemComponentsPersistence {
) )
} }
static func makeBatteryDraft(
from item: ComponentLibraryItem,
for system: ElectricalSystem,
existingLoads: [SavedLoad],
existingBatteries: [SavedBattery],
existingChargers: [SavedCharger]
) -> BatteryConfiguration {
let localizedName = item.localizedName
let baseName = localizedName.isEmpty
? String(localized: "battery.editor.default_name", defaultValue: "New Battery")
: localizedName
let batteryName = uniqueName(
startingWith: baseName,
loads: existingLoads,
batteries: existingBatteries,
chargers: existingChargers
)
let nominalVoltage = item.displayVoltage ?? 12.8
let capacity = item.capacityAmpHours ?? 100
return BatteryConfiguration(
name: batteryName,
nominalVoltage: nominalVoltage,
capacityAmpHours: capacity,
chemistry: .lithiumIronPhosphate,
iconName: "battery.100",
colorName: system.colorName,
system: system,
componentID: item.id
)
}
static func makeChargerDraft(
from item: ComponentLibraryItem,
for system: ElectricalSystem,
existingLoads: [SavedLoad],
existingBatteries: [SavedBattery],
existingChargers: [SavedCharger]
) -> ChargerConfiguration {
let localizedName = item.localizedName
let baseName = localizedName.isEmpty
? String(localized: "charger.editor.default_name", defaultValue: "New Charger")
: localizedName
let chargerName = uniqueName(
startingWith: baseName,
loads: existingLoads,
batteries: existingBatteries,
chargers: existingChargers
)
let inputVoltage = item.voltageIn ?? LocaleDefaults.mainsVoltage
let outputVoltage = item.voltageOut ?? 14.2
let power = item.watt ?? 0
let current = item.outputCurrent ?? (outputVoltage > 0 ? power / outputVoltage : 30)
let sourceType = chargerSourceType(forCategory: item.componentCategory)
return ChargerConfiguration(
name: chargerName,
inputVoltage: inputVoltage,
outputVoltage: outputVoltage,
maxCurrentAmps: current,
maxPowerWatts: power,
iconName: sourceType.iconName,
colorName: system.colorName,
system: system,
powerSourceType: sourceType,
componentID: item.id,
remoteIconURLString: item.iconURL?.absoluteString
)
}
/// Maps a PocketBase `component_category` to a charger power source.
static func chargerSourceType(forCategory category: String?) -> SavedCharger.PowerSourceType {
guard let category = category?.lowercased(), !category.isEmpty else { return .shore }
if category.contains("solar") { return .solar }
if category.contains("wind") { return .wind }
if category.contains("dcdc") || category.contains("alternator") { return .alternator }
if category.contains("generator") { return .generator }
if category.contains("mains") || category.contains("shore") { return .shore }
return .shore
}
static func makeChargerDraft( static func makeChargerDraft(
for system: ElectricalSystem, for system: ElectricalSystem,
existingLoads: [SavedLoad], existingLoads: [SavedLoad],
@@ -160,7 +241,8 @@ struct SystemComponentsPersistence {
maximumTemperatureCelsius: configuration.maximumTemperatureCelsius, maximumTemperatureCelsius: configuration.maximumTemperatureCelsius,
iconName: configuration.iconName, iconName: configuration.iconName,
colorName: configuration.colorName, colorName: configuration.colorName,
system: system system: system,
componentID: configuration.componentID
) )
context.insert(newBattery) context.insert(newBattery)
} }
@@ -184,7 +266,9 @@ struct SystemComponentsPersistence {
maxPowerWatts: configuration.maxPowerWatts, maxPowerWatts: configuration.maxPowerWatts,
iconName: configuration.iconName, iconName: configuration.iconName,
colorName: configuration.colorName, colorName: configuration.colorName,
system: system system: system,
remoteIconURLString: configuration.remoteIconURLString,
componentID: configuration.componentID
) )
context.insert(newCharger) context.insert(newCharger)
} }

View File

@@ -19,8 +19,10 @@ import androidx.compose.material.icons.outlined.BatteryChargingFull
import androidx.compose.material.icons.outlined.BatteryFull import androidx.compose.material.icons.outlined.BatteryFull
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.LibraryBooks
import androidx.compose.material.icons.outlined.Speed import androidx.compose.material.icons.outlined.Speed
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -63,6 +65,7 @@ fun BatteriesTab(
state: DetailState, state: DetailState,
onEditBattery: (String) -> Unit, onEditBattery: (String) -> Unit,
onNewBattery: () -> Unit, onNewBattery: () -> Unit,
onOpenLibrary: () -> Unit,
onDeleteBattery: (SavedBattery) -> Unit, onDeleteBattery: (SavedBattery) -> Unit,
) { ) {
val batteries = state.batteries val batteries = state.batteries
@@ -73,11 +76,15 @@ fun BatteriesTab(
subtitle = stringResource(R.string.battery_onboarding_subtitle), subtitle = stringResource(R.string.battery_onboarding_subtitle),
primaryLabel = stringResource(R.string.battery_empty_create), primaryLabel = stringResource(R.string.battery_empty_create),
onPrimary = onNewBattery, onPrimary = onNewBattery,
secondaryLabel = stringResource(R.string.loads_empty_library),
onSecondary = onOpenLibrary,
images = listOf(R.drawable.onboarding_battery),
) )
return return
} }
val m = state.metrics val m = state.metrics
Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
StatsHeader { StatsHeader {
Text(stringResource(R.string.battery_bank_header_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.battery_bank_header_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
@@ -101,12 +108,20 @@ fun BatteriesTab(
} }
} }
LazyColumn(Modifier.fillMaxSize(), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 24.dp)) { LazyColumn(Modifier.fillMaxSize(), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 96.dp)) {
items(batteries, key = { it.id }) { battery -> items(batteries, key = { it.id }) { battery ->
BatteryRow(battery, onClick = { onEditBattery(battery.id) }, onDelete = { onDeleteBattery(battery) }) BatteryRow(battery, onClick = { onEditBattery(battery.id) }, onDelete = { onDeleteBattery(battery) })
} }
} }
} }
ExtendedFloatingActionButton(
onClick = onOpenLibrary,
icon = { Icon(Icons.Outlined.LibraryBooks, contentDescription = null) },
text = { Text(stringResource(R.string.loads_library_button)) },
modifier = Modifier.align(Alignment.BottomEnd).padding(24.dp),
)
}
} }
@Composable @Composable

View File

@@ -28,6 +28,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -90,6 +91,9 @@ fun BatteryEditorScreen(systemId: String, batteryId: String?, onBack: () -> Unit
title = { title = {
Text(s.name, fontWeight = FontWeight.SemiBold, modifier = Modifier.clickable { showAppearance = true }) Text(s.name, fontWeight = FontWeight.SemiBold, modifier = Modifier.clickable { showAppearance = true })
}, },
actions = {
TextButton(onClick = onBack) { Text(stringResource(R.string.action_save)) }
},
) )
}, },
) { padding -> ) { padding ->

View File

@@ -81,6 +81,9 @@ fun ChargerEditorScreen(systemId: String, chargerId: String?, onBack: () -> Unit
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.action_back)) } IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.action_back)) }
}, },
title = { Text(s.name, fontWeight = FontWeight.SemiBold, modifier = Modifier.clickable { showAppearance = true }) }, title = { Text(s.name, fontWeight = FontWeight.SemiBold, modifier = Modifier.clickable { showAppearance = true }) },
actions = {
TextButton(onClick = onBack) { Text(stringResource(R.string.action_save)) }
},
) )
}, },
) { padding -> ) { padding ->

View File

@@ -2,6 +2,7 @@ package app.voltplan.cable.ui.chargers
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -15,7 +16,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.BatteryChargingFull import androidx.compose.material.icons.outlined.BatteryChargingFull
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.LibraryBooks
import androidx.compose.material.icons.outlined.Speed import androidx.compose.material.icons.outlined.Speed
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -55,6 +58,7 @@ fun ChargersTab(
state: DetailState, state: DetailState,
onEditCharger: (String) -> Unit, onEditCharger: (String) -> Unit,
onNewCharger: () -> Unit, onNewCharger: () -> Unit,
onOpenLibrary: () -> Unit,
onDeleteCharger: (SavedCharger) -> Unit, onDeleteCharger: (SavedCharger) -> Unit,
) { ) {
val chargers = state.chargers val chargers = state.chargers
@@ -65,11 +69,15 @@ fun ChargersTab(
subtitle = stringResource(R.string.chargers_onboarding_subtitle), subtitle = stringResource(R.string.chargers_onboarding_subtitle),
primaryLabel = stringResource(R.string.chargers_onboarding_primary), primaryLabel = stringResource(R.string.chargers_onboarding_primary),
onPrimary = onNewCharger, onPrimary = onNewCharger,
secondaryLabel = stringResource(R.string.loads_empty_library),
onSecondary = onOpenLibrary,
images = listOf(R.drawable.onboarding_charger),
) )
return return
} }
val m = state.metrics val m = state.metrics
Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
StatsHeader { StatsHeader {
Text(stringResource(R.string.chargers_summary_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.chargers_summary_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
@@ -85,12 +93,20 @@ fun ChargersTab(
SummaryMetric(Icons.Outlined.Bolt, "${Fmt.number(m.totalChargerPower)} W", stringResource(R.string.chargers_metric_power), SysPink) SummaryMetric(Icons.Outlined.Bolt, "${Fmt.number(m.totalChargerPower)} W", stringResource(R.string.chargers_metric_power), SysPink)
} }
} }
LazyColumn(Modifier.fillMaxSize(), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 24.dp)) { LazyColumn(Modifier.fillMaxSize(), contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 96.dp)) {
items(chargers, key = { it.id }) { charger -> items(chargers, key = { it.id }) { charger ->
ChargerRow(charger, onClick = { onEditCharger(charger.id) }, onDelete = { onDeleteCharger(charger) }) ChargerRow(charger, onClick = { onEditCharger(charger.id) }, onDelete = { onDeleteCharger(charger) })
} }
} }
} }
ExtendedFloatingActionButton(
onClick = onOpenLibrary,
icon = { Icon(Icons.Outlined.LibraryBooks, contentDescription = null) },
text = { Text(stringResource(R.string.loads_library_button)) },
modifier = Modifier.align(Alignment.BottomEnd).padding(24.dp),
)
}
} }
@Composable @Composable

View File

@@ -3,18 +3,17 @@ package app.voltplan.cable.ui.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -69,6 +68,7 @@ fun AppearanceEditorSheet(
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp) .padding(horizontal = 20.dp)
.padding(bottom = 24.dp), .padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
@@ -113,55 +113,41 @@ fun AppearanceEditorSheet(
extra?.invoke() extra?.invoke()
Text("Icon", style = MaterialTheme.typography.titleSmall) Text("Icon", style = MaterialTheme.typography.titleSmall)
LazyVerticalGrid( GridRows(items = icons, columns = 5) { symbol ->
columns = GridCells.Fixed(5), val selected = symbol == icon
modifier = Modifier.fillMaxWidth().heightForRows(icons.size, 5), Box(
horizontalArrangement = Arrangement.spacedBy(12.dp), Modifier
verticalArrangement = Arrangement.spacedBy(12.dp), .weight(1f)
userScrollEnabled = false, .aspectRatio(1f)
) { .clip(RoundedCornerShape(12.dp))
items(icons) { symbol -> .background(if (selected) selectedColor else MaterialTheme.colorScheme.surfaceVariant)
val selected = symbol == icon .clickable { icon = symbol },
Box( contentAlignment = Alignment.Center,
Modifier ) {
.aspectRatio(1f) Icon(
.clip(RoundedCornerShape(12.dp)) sfSymbol(symbol),
.background(if (selected) selectedColor else MaterialTheme.colorScheme.surfaceVariant) contentDescription = symbol,
.clickable { icon = symbol }, tint = if (selected) Color.White else MaterialTheme.colorScheme.onSurface,
contentAlignment = Alignment.Center, modifier = Modifier.size(24.dp),
) { )
Icon(
sfSymbol(symbol),
contentDescription = symbol,
tint = if (selected) Color.White else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(24.dp),
)
}
} }
} }
Text("Color", style = MaterialTheme.typography.titleSmall) Text("Color", style = MaterialTheme.typography.titleSmall)
LazyVerticalGrid( GridRows(items = curatedColorNames, columns = 6) { colorName ->
columns = GridCells.Fixed(6), val c = componentColor(colorName)
modifier = Modifier.fillMaxWidth().heightForRows(curatedColorNames.size, 6), Box(
horizontalArrangement = Arrangement.spacedBy(12.dp), Modifier
verticalArrangement = Arrangement.spacedBy(12.dp), .weight(1f)
userScrollEnabled = false, .aspectRatio(1f)
) { .clip(CircleShape)
items(curatedColorNames) { colorName -> .background(c)
val c = componentColor(colorName) .border(2.dp, if (colorName == color) Color.White else Color.Transparent, CircleShape)
Box( .clickable { color = colorName },
Modifier contentAlignment = Alignment.Center,
.aspectRatio(1f) ) {
.clip(CircleShape) if (colorName == color) {
.background(c) Icon(Icons.Filled.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
.border(2.dp, if (colorName == color) Color.White else Color.Transparent, CircleShape)
.clickable { color = colorName },
contentAlignment = Alignment.Center,
) {
if (colorName == color) {
Icon(Icons.Filled.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(20.dp))
}
} }
} }
} }
@@ -169,8 +155,25 @@ fun AppearanceEditorSheet(
} }
} }
private fun Modifier.heightForRows(itemCount: Int, columns: Int): Modifier { /**
val rows = (itemCount + columns - 1) / columns * Lays out [items] in a non-lazy grid of fixed [columns], sizing to its content so it can live
// ~56dp per cell including spacing; gives a non-scrolling grid inside the sheet. * inside a vertically scrolling container without a fixed height. Empty trailing cells are padded
return this.height((rows * 56).dp) * with spacers so cells keep equal widths on the final row.
*/
@Composable
private fun <T> GridRows(
items: List<T>,
columns: Int,
cell: @Composable RowScope.(T) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
items.chunked(columns).forEach { rowItems ->
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
rowItems.forEach { cell(it) }
repeat(columns - rowItems.size) {
androidx.compose.foundation.layout.Spacer(Modifier.weight(1f))
}
}
}
}
} }

View File

@@ -110,6 +110,9 @@ fun CalculatorScreen(systemId: String, loadId: String?, onBack: () -> Unit) {
Text(s.loadName, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(start = 8.dp)) Text(s.loadName, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(start = 8.dp))
} }
}, },
actions = {
TextButton(onClick = onBack) { Text(stringResource(R.string.action_save)) }
},
) )
}, },
) { padding -> ) { padding ->

View File

@@ -69,6 +69,7 @@ fun ComponentsTab(
onPrimary = onNewLoad, onPrimary = onNewLoad,
secondaryLabel = stringResource(R.string.loads_empty_library), secondaryLabel = stringResource(R.string.loads_empty_library),
onSecondary = onOpenLibrary, onSecondary = onOpenLibrary,
images = listOf(R.drawable.onboarding_coffee, R.drawable.onboarding_router, R.drawable.onboarding_charger),
) )
return return
} }

View File

@@ -8,6 +8,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import app.voltplan.cable.ui.batteries.BatteryEditorScreen import app.voltplan.cable.ui.batteries.BatteryEditorScreen
import app.voltplan.cable.ui.bom.BillOfMaterialsScreen import app.voltplan.cable.ui.bom.BillOfMaterialsScreen
import app.voltplan.cable.library.ComponentLibraryType
import app.voltplan.cable.ui.chargers.ChargerEditorScreen import app.voltplan.cable.ui.chargers.ChargerEditorScreen
import app.voltplan.cable.ui.library.ComponentLibraryScreen import app.voltplan.cable.ui.library.ComponentLibraryScreen
import app.voltplan.cable.ui.loads.CalculatorScreen import app.voltplan.cable.ui.loads.CalculatorScreen
@@ -22,11 +23,17 @@ object Routes {
const val BATTERY = "battery/{systemId}?batteryId={batteryId}" const val BATTERY = "battery/{systemId}?batteryId={batteryId}"
const val CHARGER = "charger/{systemId}?chargerId={chargerId}" const val CHARGER = "charger/{systemId}?chargerId={chargerId}"
const val BOM = "bom/{systemId}" const val BOM = "bom/{systemId}"
const val LIBRARY = "library?systemId={systemId}" const val LIBRARY = "library?systemId={systemId}&type={type}"
const val SETTINGS = "settings" const val SETTINGS = "settings"
fun system(id: String) = "system/$id" fun system(id: String) = "system/$id"
fun library(systemId: String? = null) = "library" + (systemId?.let { "?systemId=$it" } ?: "") fun library(systemId: String? = null, type: String = "load"): String {
val params = buildList {
systemId?.let { add("systemId=$it") }
add("type=$type")
}
return "library?" + params.joinToString("&")
}
fun calculator(systemId: String, loadId: String? = null) = fun calculator(systemId: String, loadId: String? = null) =
"calculator/$systemId" + (loadId?.let { "?loadId=$it" } ?: "") "calculator/$systemId" + (loadId?.let { "?loadId=$it" } ?: "")
fun battery(systemId: String, batteryId: String? = null) = fun battery(systemId: String, batteryId: String? = null) =
@@ -64,7 +71,7 @@ fun CableNavHost() {
onEditCharger = { id -> nav.navigate(Routes.charger(systemId, id)) }, onEditCharger = { id -> nav.navigate(Routes.charger(systemId, id)) },
onNewCharger = { nav.navigate(Routes.charger(systemId)) }, onNewCharger = { nav.navigate(Routes.charger(systemId)) },
onOpenBom = { nav.navigate(Routes.bom(systemId)) }, onOpenBom = { nav.navigate(Routes.bom(systemId)) },
onOpenLibrary = { nav.navigate(Routes.library(systemId)) }, onOpenLibrary = { type -> nav.navigate(Routes.library(systemId, type.typeValue)) },
) )
} }
@@ -122,15 +129,27 @@ fun CableNavHost() {
composable( composable(
Routes.LIBRARY, Routes.LIBRARY,
arguments = listOf(navArgument("systemId") { type = NavType.StringType; nullable = true; defaultValue = null }), arguments = listOf(
navArgument("systemId") { type = NavType.StringType; nullable = true; defaultValue = null },
navArgument("type") { type = NavType.StringType; nullable = true; defaultValue = "load" },
),
) { entry -> ) { entry ->
ComponentLibraryScreen( ComponentLibraryScreen(
targetSystemId = entry.arguments?.getString("systemId"), targetSystemId = entry.arguments?.getString("systemId"),
libraryType = ComponentLibraryType.fromArg(entry.arguments?.getString("type")),
onBack = { nav.popBackStack() }, onBack = { nav.popBackStack() },
onOpenSystem = { systemId -> onOpenSystem = { systemId ->
nav.popBackStack() nav.popBackStack()
nav.navigate(Routes.system(systemId)) nav.navigate(Routes.system(systemId))
}, },
onOpenBatteryEditor = { systemId, batteryId ->
nav.popBackStack()
nav.navigate(Routes.battery(systemId, batteryId))
},
onOpenChargerEditor = { systemId, chargerId ->
nav.popBackStack()
nav.navigate(Routes.charger(systemId, chargerId))
},
) )
} }

View File

@@ -58,6 +58,9 @@ fun OverviewTab(
onAddCharger: () -> Unit, onAddCharger: () -> Unit,
onOpenLibrary: () -> Unit, onOpenLibrary: () -> Unit,
onOpenBom: () -> Unit, onOpenBom: () -> Unit,
onSelectLoads: () -> Unit,
onSelectBatteries: () -> Unit,
onSelectChargers: () -> Unit,
onSetRuntimeGoal: (Double?) -> Unit, onSetRuntimeGoal: (Double?) -> Unit,
onSetChargeGoal: (Double?) -> Unit, onSetChargeGoal: (Double?) -> Unit,
) { ) {
@@ -104,9 +107,9 @@ fun OverviewTab(
} }
} }
LoadsCard(state, m, onAddLoad, onOpenLibrary) LoadsCard(state, m, onAddLoad, onOpenLibrary, onSelectLoads)
BatteriesCard(state, m, onAddBattery) BatteriesCard(state, m, onAddBattery, onSelectBatteries)
ChargersCard(state, m, onAddCharger) ChargersCard(state, m, onAddCharger, onSelectChargers)
} }
goalEditor?.let { kind -> goalEditor?.let { kind ->
@@ -163,9 +166,10 @@ private fun MetricRow(
} }
@Composable @Composable
private fun OverviewCard(title: String, content: @Composable () -> Unit) { private fun OverviewCard(title: String, onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
Surface( Surface(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)
.then(if (onClick != null) Modifier.clickable { onClick() } else Modifier),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
) { ) {
@@ -177,8 +181,8 @@ private fun OverviewCard(title: String, content: @Composable () -> Unit) {
} }
@Composable @Composable
private fun LoadsCard(state: DetailState, m: SystemMetrics, onAddLoad: () -> Unit, onOpenLibrary: () -> Unit) { private fun LoadsCard(state: DetailState, m: SystemMetrics, onAddLoad: () -> Unit, onOpenLibrary: () -> Unit, onSelect: () -> Unit) {
OverviewCard(stringResource(R.string.loads_overview_header_title)) { OverviewCard(stringResource(R.string.loads_overview_header_title), onClick = if (state.loads.isEmpty()) null else onSelect) {
if (state.loads.isEmpty()) { if (state.loads.isEmpty()) {
Text(stringResource(R.string.overview_loads_empty_title), fontWeight = FontWeight.Medium) Text(stringResource(R.string.overview_loads_empty_title), fontWeight = FontWeight.Medium)
Text(stringResource(R.string.overview_loads_empty_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.overview_loads_empty_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
@@ -196,8 +200,8 @@ private fun LoadsCard(state: DetailState, m: SystemMetrics, onAddLoad: () -> Uni
} }
@Composable @Composable
private fun BatteriesCard(state: DetailState, m: SystemMetrics, onAddBattery: () -> Unit) { private fun BatteriesCard(state: DetailState, m: SystemMetrics, onAddBattery: () -> Unit, onSelect: () -> Unit) {
OverviewCard(stringResource(R.string.battery_bank_header_title)) { OverviewCard(stringResource(R.string.battery_bank_header_title), onClick = if (state.batteries.isEmpty()) null else onSelect) {
if (state.batteries.isEmpty()) { if (state.batteries.isEmpty()) {
Text(stringResource(R.string.battery_empty_title), fontWeight = FontWeight.Medium) Text(stringResource(R.string.battery_empty_title), fontWeight = FontWeight.Medium)
Button(onClick = onAddBattery) { Text(stringResource(R.string.battery_empty_create)) } Button(onClick = onAddBattery) { Text(stringResource(R.string.battery_empty_create)) }
@@ -212,8 +216,8 @@ private fun BatteriesCard(state: DetailState, m: SystemMetrics, onAddBattery: ()
} }
@Composable @Composable
private fun ChargersCard(state: DetailState, m: SystemMetrics, onAddCharger: () -> Unit) { private fun ChargersCard(state: DetailState, m: SystemMetrics, onAddCharger: () -> Unit, onSelect: () -> Unit) {
OverviewCard(stringResource(R.string.overview_chargers_header_title)) { OverviewCard(stringResource(R.string.overview_chargers_header_title), onClick = if (state.chargers.isEmpty()) null else onSelect) {
if (state.chargers.isEmpty()) { if (state.chargers.isEmpty()) {
Text(stringResource(R.string.overview_chargers_empty_title), fontWeight = FontWeight.Medium) Text(stringResource(R.string.overview_chargers_empty_title), fontWeight = FontWeight.Medium)
Text(stringResource(R.string.overview_chargers_empty_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.overview_chargers_empty_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -16,7 +17,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.ChevronRight import androidx.compose.material.icons.outlined.ChevronRight
import androidx.compose.material.icons.outlined.AutoAwesome
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import app.voltplan.cable.R import app.voltplan.cable.R
import app.voltplan.cable.ui.components.OnboardingCarousel
import app.voltplan.cable.ui.sfSymbol import app.voltplan.cable.ui.sfSymbol
import app.voltplan.cable.ui.theme.componentColor import app.voltplan.cable.ui.theme.componentColor
@@ -174,7 +175,10 @@ private fun SystemsOnboarding(modifier: Modifier = Modifier, onCreate: (String)
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Icon(Icons.Outlined.AutoAwesome, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(48.dp)) OnboardingCarousel(
images = listOf(R.drawable.onboarding_van, R.drawable.onboarding_cabin, R.drawable.onboarding_boat),
modifier = Modifier.fillMaxWidth().height(220.dp),
)
Spacer(Modifier.size(16.dp)) Spacer(Modifier.size(16.dp))
Text(stringResource(R.string.onboarding_systems_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.onboarding_systems_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))