Queue document writes durably and surface sync trouble in the UI
iCloud Drive writes now flow through a persistent WriteBacklog sidecar (drained with backoff, flushed on backgrounding, wiped with the cache on account change), so a save can never be lost to a transient coordinator error. A status banner on the workout list surfaces stuck syncing. Also: the split picker gains a Recent section with day labels, split rows fold SplitItem into SplitListView, and list rows dim the multiply sign in sets-by-reps. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
//
|
||||
// SplitItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/18/25 at 2:45 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SplitItem: View {
|
||||
var split: Split
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack(alignment: .bottom) {
|
||||
// Golden ratio rectangle (1:1.618)
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [splitColor, splitColor.darker(by: 0.2)]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.aspectRatio(1.618, contentMode: .fit)
|
||||
.shadow(radius: 2)
|
||||
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 4) {
|
||||
Spacer()
|
||||
|
||||
// Icon in the center - using dynamic sizing
|
||||
Image(systemName: split.systemImage)
|
||||
.font(.system(size: min(geo.size.width * 0.3, 40), weight: .bold))
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: geo.size.width * 0.6, maxHeight: geo.size.height * 0.4)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Name at the bottom inside the rectangle
|
||||
Text(split.name)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
Text("\(split.exercisesArray.count) exercises")
|
||||
.font(.caption)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var splitColor: Color {
|
||||
Color.color(from: split.color)
|
||||
}
|
||||
}
|
||||
@@ -10,39 +10,37 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
/// The Splits library screen, pushed from Settings → Library: a two-column grid of
|
||||
/// split tiles. Tapping a tile opens `SplitDetailView`; long-pressing offers Delete
|
||||
/// (the sole split-delete affordance); the toolbar's + adds a new split. Starter
|
||||
/// splits are seeded automatically on the true first run (`SyncEngine.autoSeedIfEmpty`),
|
||||
/// so an empty library just invites creating one.
|
||||
/// The Splits library screen, pushed from Settings → Library: a plain list of
|
||||
/// splits, one row each. Tapping a row opens `SplitDetailView`; long-pressing
|
||||
/// offers Delete (the sole split-delete affordance); the toolbar's + adds a new
|
||||
/// split. Starter splits are seeded automatically on the true first run
|
||||
/// (`SyncEngine.autoSeedIfEmpty`), so an empty library just invites creating one.
|
||||
struct SplitListView: View {
|
||||
@Environment(SyncEngine.self) private var sync
|
||||
|
||||
@Query(sort: \Split.order) private var splits: [Split]
|
||||
@Query(sort: \Split.name) private var splits: [Split]
|
||||
|
||||
@State private var showingAddSheet = false
|
||||
@State private var splitToDelete: Split?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
|
||||
ForEach(splits) { split in
|
||||
NavigationLink {
|
||||
SplitDetailView(split: split)
|
||||
List {
|
||||
ForEach(splits) { split in
|
||||
NavigationLink {
|
||||
SplitDetailView(split: split)
|
||||
} label: {
|
||||
SplitRow(split: split)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
splitToDelete = split
|
||||
} label: {
|
||||
SplitItem(split: split)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
splitToDelete = split
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.overlay {
|
||||
if splits.isEmpty {
|
||||
ContentUnavailableView(
|
||||
@@ -87,3 +85,34 @@ struct SplitListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single split row: a small rounded-square badge (the split's color at low
|
||||
/// opacity, its symbol tinted full-strength) beside the split name and its
|
||||
/// exercise count.
|
||||
private struct SplitRow: View {
|
||||
var split: Split
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: split.systemImage)
|
||||
.font(.title3)
|
||||
.foregroundStyle(splitColor)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(splitColor.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(split.name)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text("\(split.exercisesArray.count) exercises")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private var splitColor: Color {
|
||||
Color.color(from: split.color)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user