diff --git a/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..04056a5 --- /dev/null +++ b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "vision", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Contents.json b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Contents.json new file mode 100644 index 0000000..bb816da --- /dev/null +++ b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.solidimagestacklayer" + }, + { + "filename" : "Back.solidimagestacklayer" + } + ] +} diff --git a/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..04056a5 --- /dev/null +++ b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "vision", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/trivit Vision/Images.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/trivit Vision/Images.xcassets/Contents.json b/trivit Vision/Images.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/trivit Vision/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/trivit Vision/Info.plist b/trivit Vision/Info.plist new file mode 100644 index 0000000..165cfc2 --- /dev/null +++ b/trivit Vision/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Trivit + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 5.0.1 + CFBundleVersion + 2 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + + diff --git a/trivit Vision/Models/Trivit.swift b/trivit Vision/Models/Trivit.swift new file mode 100644 index 0000000..d64fd09 --- /dev/null +++ b/trivit Vision/Models/Trivit.swift @@ -0,0 +1,56 @@ +// +// Trivit.swift +// trivit Vision +// +// Simplified SwiftData model for visionOS (matches watch app pattern) +// + +import Foundation +import SwiftData + +@Model +final class Trivit: Equatable { + var id: UUID + var title: String + var count: Int + var colorIndex: Int + var isCollapsed: Bool + var createdAt: Date + var sortOrder: Int + + init( + id: UUID = UUID(), + title: String = "New Trivit", + count: Int = 0, + colorIndex: Int = 0, + isCollapsed: Bool = true, + createdAt: Date = Date(), + sortOrder: Int = 0 + ) { + self.id = id + self.title = title + self.count = count + self.colorIndex = colorIndex + self.isCollapsed = isCollapsed + self.createdAt = createdAt + self.sortOrder = sortOrder + } + + func increment() { + count += 1 + } + + func decrement() { + if count > 0 { + count -= 1 + } + } + + func reset() { + count = 0 + } + + static func == (lhs: Trivit, rhs: Trivit) -> Bool { + lhs.id == rhs.id + } +} diff --git a/trivit Vision/Theme/TrivitColors.swift b/trivit Vision/Theme/TrivitColors.swift new file mode 100644 index 0000000..cdae667 --- /dev/null +++ b/trivit Vision/Theme/TrivitColors.swift @@ -0,0 +1,62 @@ +// +// TrivitColors.swift +// trivit Vision +// +// Color themes for the visionOS app - matching iOS app design +// + +import SwiftUI + +struct TrivitColors { + static let colorCount = 10 + + // Main color palette (flat design inspired) - same as iOS app + static let palette: [Color] = [ + Color(hex: "1ABC9C"), // Turquoise + Color(hex: "2ECC71"), // Emerald + Color(hex: "3498DB"), // Peter River + Color(hex: "9B59B6"), // Amethyst + Color(hex: "E74C3C"), // Alizarin + Color(hex: "F39C12"), // Orange + Color(hex: "E91E63"), // Pink + Color(hex: "00BCD4"), // Cyan + Color(hex: "8BC34A"), // Light Green + Color(hex: "FF5722"), // Deep Orange + ] + + static func color(at index: Int) -> Color { + let safeIndex = abs(index) % palette.count + return palette[safeIndex] + } + + static func randomColorIndex() -> Int { + Int.random(in: 0..> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } +} diff --git a/trivit Vision/TrivitVisionApp.entitlements b/trivit Vision/TrivitVisionApp.entitlements new file mode 100644 index 0000000..2158655 --- /dev/null +++ b/trivit Vision/TrivitVisionApp.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.icloud-container-identifiers + + iCloud.com.wouterdevriendt.trivit + + + diff --git a/trivit Vision/TrivitVisionApp.swift b/trivit Vision/TrivitVisionApp.swift new file mode 100644 index 0000000..b6a8d2d --- /dev/null +++ b/trivit Vision/TrivitVisionApp.swift @@ -0,0 +1,64 @@ +// +// TrivitVisionApp.swift +// trivit Vision +// +// visionOS app entry point - syncs with iOS via CloudKit +// + +import SwiftUI +import SwiftData +import os.log + +private let logger = Logger(subsystem: "com.wouterdevriendt.trivit.vision", category: "VisionApp") + +@main +struct TrivitVisionApp: App { + // Check for sample data mode (for screenshots) + private static var isSampleDataMode: Bool { + ProcessInfo.processInfo.arguments.contains("-SampleDataMode") + } + + var sharedModelContainer: ModelContainer = { + let schema = Schema([Trivit.self]) + + let modelConfiguration = ModelConfiguration( + schema: schema, + isStoredInMemoryOnly: isSampleDataMode, + cloudKitDatabase: isSampleDataMode ? .none : .automatic + ) + + do { + logger.info("🥽 Creating ModelContainer for visionOS app") + let container = try ModelContainer(for: schema, configurations: [modelConfiguration]) + + if isSampleDataMode { + logger.info("🥽 Sample data mode - creating sample trivits") + let context = container.mainContext + let sampleTrivits = [ + Trivit(title: "Glasses of water", count: 7, colorIndex: 0, sortOrder: 0), + Trivit(title: "Push-ups done", count: 42, colorIndex: 1, sortOrder: 1), + Trivit(title: "Books read", count: 3, colorIndex: 2, sortOrder: 2), + Trivit(title: "Meditation", count: 15, colorIndex: 4, sortOrder: 3), + Trivit(title: "Coffee cups", count: 5, colorIndex: 5, sortOrder: 4), + Trivit(title: "Steps walked", count: 12, colorIndex: 3, sortOrder: 5), + ] + for trivit in sampleTrivits { + context.insert(trivit) + } + try? context.save() + } + + return container + } catch { + logger.error("🥽 Failed to create ModelContainer: \(error)") + fatalError("Could not create ModelContainer: \(error)") + } + }() + + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(sharedModelContainer) + } +} diff --git a/trivit Vision/Views/AddTrivitView.swift b/trivit Vision/Views/AddTrivitView.swift new file mode 100644 index 0000000..e5d6b94 --- /dev/null +++ b/trivit Vision/Views/AddTrivitView.swift @@ -0,0 +1,84 @@ +// +// AddTrivitView.swift +// trivit Vision +// +// Sheet for creating a new counter +// + +import SwiftUI +import SwiftData + +struct AddTrivitView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + @Query(sort: \Trivit.sortOrder) private var trivits: [Trivit] + @State private var title = "" + @State private var selectedColorIndex = 0 + + var body: some View { + NavigationStack { + Form { + Section("Name") { + TextField("Counter name", text: $title) + } + + Section("Color") { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 12) { + ForEach(0.. 0 { + TallyMarksView(count: trivit.count) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(minHeight: 24) + } + + // +/- buttons + HStack(spacing: 16) { + Button { + trivit.decrement() + } label: { + Image(systemName: "minus") + .font(.title3.weight(.semibold)) + .frame(width: 44, height: 44) + } + .buttonStyle(.bordered) + .disabled(trivit.count == 0) + .hoverEffect(.highlight) + + Spacer() + + Button { + trivit.increment() + } label: { + Image(systemName: "plus") + .font(.title3.weight(.semibold)) + .frame(width: 44, height: 44) + } + .buttonStyle(.borderedProminent) + .tint(accentColor) + .hoverEffect(.highlight) + } + } + .padding(20) + } + .glassBackgroundEffect() + .clipShape(RoundedRectangle(cornerRadius: 16)) + .hoverEffect(.highlight) + .contextMenu { + Button { + isEditing = true + } label: { + Label("Rename", systemImage: "pencil") + } + Button { + trivit.colorIndex = (trivit.colorIndex + 1) % TrivitColors.colorCount + } label: { + Label("Change Color", systemImage: "paintpalette") + } + Button(role: .destructive) { + showResetConfirmation = true + } label: { + Label("Reset Count", systemImage: "arrow.counterclockwise") + } + Divider() + Button(role: .destructive) { + modelContext.delete(trivit) + } label: { + Label("Delete", systemImage: "trash") + } + } + .confirmationDialog("Reset Counter?", isPresented: $showResetConfirmation) { + Button("Reset to Zero", role: .destructive) { + trivit.reset() + } + Button("Cancel", role: .cancel) {} + } + } +} + +#Preview { + CounterCardView(trivit: Trivit(title: "Push-ups", count: 42, colorIndex: 1)) + .frame(width: 320) + .modelContainer(for: [Trivit.self], inMemory: true) +} diff --git a/trivit Vision/Views/TallyMarksView.swift b/trivit Vision/Views/TallyMarksView.swift new file mode 100644 index 0000000..303c3be --- /dev/null +++ b/trivit Vision/Views/TallyMarksView.swift @@ -0,0 +1,71 @@ +// +// TallyMarksView.swift +// trivit Vision +// +// Western-style tally marks for visionOS spatial UI +// + +import SwiftUI + +struct TallyMarksView: View { + let count: Int + private let groupsPerRow = 10 + + var body: some View { + let fullGroups = count / 5 + let remainder = count % 5 + let totalGroups = fullGroups + (remainder > 0 ? 1 : 0) + let rows = max(1, (totalGroups + groupsPerRow - 1) / groupsPerRow) + + VStack(alignment: .leading, spacing: 8) { + ForEach(0..