Give each Monero account a swipeable card with its balance and QR

This commit is contained in:
2026-10-01 22:05:19 +08:00
parent e3846fe881
commit 994ba5ed6b
4 changed files with 189 additions and 50 deletions
+78 -16
View File
@@ -90,6 +90,8 @@ struct HomeView: View {
@State private var showDeleteConfirm = false
@State private var selectedTransaction: TransactionRecord?
@State private var showSettings = false
@State private var activeAccount = 0
@State private var qrAddress: AddressQR?
var body: some View {
VStack(spacing: 18) {
@@ -115,6 +117,9 @@ struct HomeView: View {
symbol: model.symbol,
chain: model.chain) { selectedTransaction = nil }
}
.sheet(item: $qrAddress) { item in
AddressQRSheet(title: item.title, address: item.address) { qrAddress = nil }
}
.sheet(isPresented: $showSettings) {
SettingsView()
.environmentObject(model)
@@ -176,26 +181,38 @@ struct HomeView: View {
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Balance")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
Text("\(model.balance.total) \(model.symbol)")
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if let fiat = model.fiatBalance {
Text("\(fiat)")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Theme.muted)
if model.chain == .xmr, !model.moneroAccounts.isEmpty {
// Cake Wallet style: one card per account, swiped horizontally.
TabView(selection: $activeAccount) {
ForEach(model.moneroAccounts) { account in
moneroAccountCard(account)
.tag(account.index)
}
}
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
Text("\(model.balance.unlocked) \(model.symbol) unlocked")
.font(.system(size: 11))
.tabViewStyle(.page(indexDisplayMode: .always))
.frame(height: 208)
} else {
VStack(alignment: .leading, spacing: 4) {
Text("Balance")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Theme.muted)
Text("\(model.balance.total) \(model.symbol)")
.font(.system(size: 30, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if let fiat = model.fiatBalance {
Text("\(fiat)")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Theme.muted)
}
if model.chain == .xmr, model.balance.unlocked != model.balance.total {
Text("\(model.balance.unlocked) \(model.symbol) unlocked")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
}
AddressRow(address: model.displayAddress)
AddressRow(address: model.displayAddress)
}
if model.chain == .xmr, let status = model.syncStatus {
SyncProgress(status: status, eta: model.syncEta)
@@ -270,6 +287,51 @@ struct HomeView: View {
}
}
private func moneroAccountCard(_ account: MoneroAccount) -> some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text(account.title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
Spacer()
Text("Account \(account.index + 1) of \(model.moneroAccounts.count)")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
VStack(alignment: .leading, spacing: 3) {
Text("\(account.balance) XMR")
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundStyle(Theme.text)
if let fiat = model.fiatValue(of: account.balance, chain: .xmr) {
Text(fiat)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Theme.muted)
}
if account.unlocked != account.balance {
Text("\(account.unlocked) XMR unlocked")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
}
}
HStack(spacing: 8) {
AddressRow(address: account.address)
Button {
qrAddress = AddressQR(title: account.title, address: account.address)
} label: {
Image(systemName: "qrcode")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.accent)
.frame(width: 36, height: 36)
.background(Theme.surfaceAlt)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
}
.padding(.bottom, 18)
}
private var moneroUnavailableCard: some View {
VStack(spacing: 14) {
ChainPicker(selection: Binding(get: { model.chain },
+37 -1
View File
@@ -4,12 +4,48 @@ struct SettingsView: View {
@EnvironmentObject private var model: AppModel
@Environment(\.dismiss) private var dismiss
private let currencies = [
"USD", "EUR", "GBP", "CHF", "JPY", "CNY", "CAD", "AUD",
"NZD", "SGD", "HKD", "KRW", "INR", "BRL", "SEK", "NOK",
"DKK", "MXN", "ZAR", "TRY",
]
var body: some View {
ZStack {
Theme.background.ignoresSafeArea()
VStack(alignment: .leading, spacing: 18) {
ScreenHeader(title: "Settings", subtitle: "Security and device options", onBack: { dismiss() })
Card {
VStack(alignment: .leading, spacing: 10) {
Text("Currency")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(Theme.muted)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 64), spacing: 8)], spacing: 8) {
ForEach(currencies, id: \.self) { currency in
let selected = model.fiatCurrency == currency
Button {
model.fiatCurrency = currency
} label: {
Text(currency)
.font(.system(size: 13, weight: .medium))
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(selected ? Theme.accent : Theme.surfaceAlt)
.foregroundStyle(selected ? Color.black : Theme.text)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
}
}
Text("Prices come from CoinGecko (with a Kraken fallback for USD and EUR) and refresh about once a minute.")
.font(.system(size: 11))
.foregroundStyle(Theme.muted)
.padding(.top, 4)
}
}
Card {
Toggle(isOn: Binding(get: { model.faceIDEnabled },
set: { model.faceIDEnabled = $0 })) {
@@ -38,7 +74,7 @@ struct SettingsView: View {
.padding(.top, 12)
}
.preferredColorScheme(.dark)
.presentationDetents([.medium])
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
}