From 6edb9caa670e0fb02a6d7548bc6339717a7f34bf Mon Sep 17 00:00:00 2001 From: liladhar Date: Thu, 4 Jun 2026 12:12:11 +0530 Subject: [PATCH] Refactor UI components for improved aesthetics and functionality - Enhanced the layout and design of ContentView, HomeView, and ScaleView with a new glass effect and improved spacing. - Updated DebugView to use a more modern design with glass backgrounds and improved button styles. - Adjusted SettingsView to include a glass-themed header and refined device selection UI. - Improved responsiveness and visual consistency across various views. - Added default window size settings in TrackWeightApp for better user experience. --- TrackWeight.xcodeproj/project.pbxproj | 13 +- TrackWeight/ContentView.swift | 41 ++- TrackWeight/DebugView.swift | 126 ++++---- TrackWeight/GlassTheme.swift | 340 ++++++++++++++++++++++ TrackWeight/HomeView.swift | 204 ++++++------- TrackWeight/ScaleView.swift | 292 +++++++++---------- TrackWeight/SettingsView.swift | 198 ++++++------- TrackWeight/TrackWeightApp.swift | 4 + TrackWeight/TrackWeightView.swift | 397 +++++++++++++------------- 9 files changed, 973 insertions(+), 642 deletions(-) create mode 100644 TrackWeight/GlassTheme.swift diff --git a/TrackWeight.xcodeproj/project.pbxproj b/TrackWeight.xcodeproj/project.pbxproj index a343c4f..b52e409 100644 --- a/TrackWeight.xcodeproj/project.pbxproj +++ b/TrackWeight.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 93A095122E33359600E1E1D1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A095112E33359600E1E1D1 /* SettingsView.swift */; }; 93A095162E33624200E1E1D1 /* OpenMultitouchSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 93A095152E33624200E1E1D1 /* OpenMultitouchSupport */; }; 93ABD0212E2E01E200668D4F /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ABD0202E2E01E200668D4F /* HomeView.swift */; }; + 94B0A0112E40010000E1E1D1 /* GlassTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B0A0102E40010000E1E1D1 /* GlassTheme.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -39,6 +40,7 @@ 77292AA72B931E06001CA3F6 /* ScaleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleViewModel.swift; sourceTree = ""; }; 93A095112E33359600E1E1D1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 93ABD0202E2E01E200668D4F /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 94B0A0102E40010000E1E1D1 /* GlassTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassTheme.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -80,6 +82,7 @@ 77292A972B931D60001CA3F6 /* ContentViewModel.swift */, 77292A9D2B931E01001CA3F6 /* WeighingState.swift */, 93ABD0202E2E01E200668D4F /* HomeView.swift */, + 94B0A0102E40010000E1E1D1 /* GlassTheme.swift */, 77292A9F2B931E02001CA3F6 /* WeighingViewModel.swift */, 93A095112E33359600E1E1D1 /* SettingsView.swift */, 77292AA12B931E03001CA3F6 /* TrackWeightView.swift */, @@ -137,7 +140,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1520; - LastUpgradeCheck = 1620; + LastUpgradeCheck = 2650; TargetAttributes = { 77292A832B931953001CA3F6 = { CreatedOnToolsVersion = 15.2; @@ -186,6 +189,7 @@ 77292A982B931D60001CA3F6 /* ContentViewModel.swift in Sources */, 77292A882B931953001CA3F6 /* TrackWeightApp.swift in Sources */, 93ABD0212E2E01E200668D4F /* HomeView.swift in Sources */, + 94B0A0112E40010000E1E1D1 /* GlassTheme.swift in Sources */, 77292A9C2B931E01001CA3F6 /* WeighingState.swift in Sources */, 77292A9E2B931E02001CA3F6 /* WeighingViewModel.swift in Sources */, 77292AA02B931E03001CA3F6 /* TrackWeightView.swift in Sources */, @@ -259,6 +263,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -318,6 +323,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = NO; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; @@ -333,7 +339,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 9ZRLG6277G; + DEVELOPMENT_TEAM = M7X27V439V; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -345,7 +351,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.krishkrosh.trackweight; + PRODUCT_BUNDLE_IDENTIFIER = com.liladhar.trackweight; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -365,6 +371,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 9ZRLG6277G; + "DEVELOPMENT_TEAM[sdk=macosx*]" = M7X27V439V; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/TrackWeight/ContentView.swift b/TrackWeight/ContentView.swift index 7a4bfa7..b975017 100644 --- a/TrackWeight/ContentView.swift +++ b/TrackWeight/ContentView.swift @@ -7,43 +7,60 @@ import SwiftUI struct ContentView: View { @State private var showHomePage = true - @State private var selectedTab = 1 // Start with Scale tab (index 1) - + @State private var selectedTab = 1 + var body: some View { - if showHomePage { - HomeView { - showHomePage = false + Group { + if showHomePage { + HomeView { + withAnimation(.easeInOut(duration: 0.45)) { + showHomePage = false + } + } + } else { + mainTabs } - .frame(minWidth: 700, minHeight: 500) - } else { + } + .frame( + minWidth: AppLayout.windowMinWidth, + minHeight: AppLayout.windowMinHeight + ) + .preferredColorScheme(.dark) + } + + private var mainTabs: some View { + ZStack { + GlassBackground() + TabView(selection: $selectedTab) { TrackWeightView() + .frame(maxWidth: .infinity, maxHeight: .infinity) .tabItem { Image(systemName: "arrow.3.trianglepath") - Text("Guided (Experimental)") + Text("Guided") } .tag(0) - + ScaleView() + .frame(maxWidth: .infinity, maxHeight: .infinity) .tabItem { Image(systemName: "scalemass") Text("Scale") } .tag(1) - + SettingsView() + .frame(maxWidth: .infinity, maxHeight: .infinity) .tabItem { Image(systemName: "gearshape") Text("Settings") } .tag(2) } - .frame(minWidth: 700, minHeight: 500) } } } - #Preview { ContentView() } diff --git a/TrackWeight/DebugView.swift b/TrackWeight/DebugView.swift index a945a58..8f32a5b 100644 --- a/TrackWeight/DebugView.swift +++ b/TrackWeight/DebugView.swift @@ -2,8 +2,6 @@ // DebugView.swift // TrackWeight // -// Created by Takuto Nakamura on 2024/03/02. -// import OpenMultitouchSupport import SwiftUI @@ -13,80 +11,73 @@ struct DebugView: View { @Environment(\.dismiss) private var dismiss var body: some View { - VStack { - // Header with close button - HStack { - Text("Debug Console") - .font(.title2) - .fontWeight(.semibold) - - Spacer() - - Button(action: { - dismiss() - }) { - Image(systemName: "xmark.circle.fill") - .font(.title2) - .foregroundColor(.secondary) + ZStack { + GlassBackground() + + VStack(spacing: 20) { + HStack { + Text("Debug Console") + .font(.title2.weight(.bold)) + .accentGradientForeground() + + Spacer() + + Button(action: { dismiss() }) { + Image(systemName: "xmark.circle.fill") + .font(.title2) + .foregroundStyle(.white.opacity(0.5)) + } + .buttonStyle(.plain) + .help("Close Debug Console") } - .buttonStyle(PlainButtonStyle()) - .help("Close Debug Console") - } - .padding(.bottom) - // Device Selector - if !viewModel.availableDevices.isEmpty { - VStack(alignment: .leading) { - Text("Trackpad Device:") - .font(.headline) - Picker("Select Device", selection: Binding( - get: { viewModel.selectedDevice }, - set: { device in - if let device = device { - viewModel.selectDevice(device) + if !viewModel.availableDevices.isEmpty { + VStack(alignment: .leading, spacing: 10) { + Text("Trackpad Device") + .font(.headline) + .foregroundStyle(.white.opacity(0.85)) + + Picker("Select Device", selection: Binding( + get: { viewModel.selectedDevice }, + set: { device in + if let device = device { + viewModel.selectDevice(device) + } + } + )) { + ForEach(viewModel.availableDevices, id: \.self) { device in + Text("\(device.deviceName) (ID: \(device.deviceID))") + .tag(device as OMSDeviceInfo?) } } - )) { - ForEach(viewModel.availableDevices, id: \.self) { device in - Text("\(device.deviceName) (ID: \(device.deviceID))") - .tag(device as OMSDeviceInfo?) - } + .pickerStyle(.menu) + .tint(.white) } - .pickerStyle(MenuPickerStyle()) + .padding(20) + .glassCard(cornerRadius: 16) } - .padding(.bottom) - } - - if viewModel.isListening { - Button { - viewModel.stop() - } label: { - Text("Stop") - } - } else { - Button { - viewModel.start() - } label: { - Text("Start") + + if viewModel.isListening { + Button { viewModel.stop() } label: { Text("Stop") } + .buttonStyle(GlassPrimaryButtonStyle()) + } else { + Button { viewModel.start() } label: { Text("Start") } + .buttonStyle(GlassPrimaryButtonStyle()) } - } - Canvas { context, size in - viewModel.touchData.forEach { touch in - let path = makeEllipse(touch: touch, size: size) - context.fill(path, with: .color(.primary.opacity(Double(touch.total)))) + + Canvas { context, size in + viewModel.touchData.forEach { touch in + let path = makeEllipse(touch: touch, size: size) + context.fill(path, with: .color(.cyan.opacity(Double(touch.total)))) + } } + .frame(width: 600, height: 400) + .glassCard(cornerRadius: 16) } - .frame(width: 600, height: 400) - .border(Color.primary) - } - .fixedSize() - .padding() - .onAppear { - viewModel.onAppear() - } - .onDisappear { - viewModel.onDisappear() + .padding(28) } + .onAppear { viewModel.onAppear() } + .onDisappear { viewModel.onDisappear() } } private func makeEllipse(touch: OMSTouchData, size: CGSize) -> Path { @@ -104,4 +95,5 @@ struct DebugView: View { #Preview { DebugView() -} \ No newline at end of file + .frame(width: 700, height: 500) +} diff --git a/TrackWeight/GlassTheme.swift b/TrackWeight/GlassTheme.swift new file mode 100644 index 0000000..321bc74 --- /dev/null +++ b/TrackWeight/GlassTheme.swift @@ -0,0 +1,340 @@ +// +// GlassTheme.swift +// TrackWeight +// + +import SwiftUI + +enum AppLayout { + static let windowMinWidth: CGFloat = 720 + static let windowMinHeight: CGFloat = 580 + static let defaultWidth: CGFloat = 760 + static let defaultHeight: CGFloat = 600 + + static let screenInset: CGFloat = 20 + static let panelPadding: CGFloat = 32 + static let sectionSpacing: CGFloat = 24 + static let footerMinHeight: CGFloat = 72 +} + +enum AppTheme { + static let accent = LinearGradient( + colors: [ + Color(red: 0.35, green: 0.55, blue: 1.0), + Color(red: 0.25, green: 0.78, blue: 0.92), + Color(red: 0.45, green: 0.92, blue: 0.88) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + static let accentColors: [Color] = [ + Color(red: 0.35, green: 0.55, blue: 1.0), + Color(red: 0.25, green: 0.78, blue: 0.92), + Color(red: 0.45, green: 0.92, blue: 0.88) + ] + + static let warmAccent = LinearGradient( + colors: [ + Color(red: 1.0, green: 0.55, blue: 0.35), + Color(red: 1.0, green: 0.72, blue: 0.4) + ], + startPoint: .leading, + endPoint: .trailing + ) +} + +// MARK: - Background + +struct GlassBackground: View { + @State private var phase: CGFloat = 0 + + var body: some View { + ZStack { + LinearGradient( + colors: [ + Color(red: 0.06, green: 0.09, blue: 0.18), + Color(red: 0.10, green: 0.14, blue: 0.28), + Color(red: 0.08, green: 0.20, blue: 0.26) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + + Circle() + .fill( + RadialGradient( + colors: [ + Color.cyan.opacity(0.35), + Color.clear + ], + center: .center, + startRadius: 0, + endRadius: 280 + ) + ) + .frame(width: 520, height: 520) + .offset(x: -180 + phase * 40, y: -120) + .blur(radius: 2) + + Circle() + .fill( + RadialGradient( + colors: [ + Color.blue.opacity(0.4), + Color.clear + ], + center: .center, + startRadius: 0, + endRadius: 320 + ) + ) + .frame(width: 600, height: 600) + .offset(x: 220 - phase * 30, y: 180) + .blur(radius: 4) + + Circle() + .fill( + RadialGradient( + colors: [ + Color.purple.opacity(0.25), + Color.clear + ], + center: .center, + startRadius: 0, + endRadius: 240 + ) + ) + .frame(width: 400, height: 400) + .offset(x: 60, y: 280 - phase * 20) + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 8).repeatForever(autoreverses: true)) { + phase = 1 + } + } + } +} + +// MARK: - Modifiers + +struct GlassCardModifier: ViewModifier { + var cornerRadius: CGFloat = 20 + var tint: Color = .white + var borderWidth: CGFloat = 1.5 + var borderTint: Color? = nil + + func body(content: Content) -> some View { + let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + let accent = borderTint ?? .white + + content + .background { + shape + .fill(.ultraThinMaterial) + .background( + shape.fill(tint.opacity(0.08)) + ) + .overlay( + shape.stroke(accent.opacity(0.22), lineWidth: borderWidth + 0.5) + ) + .overlay( + shape.stroke( + LinearGradient( + colors: [ + Color.white.opacity(0.72), + Color.white.opacity(0.18), + accent.opacity(0.35) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: borderWidth + ) + ) + .shadow(color: Color.black.opacity(0.3), radius: 24, x: 0, y: 14) + } + } +} + +/// Full-screen inset panel with a visible border matching the window content area. +struct GlassScreenFrame: View { + @ViewBuilder var content: () -> Content + + var body: some View { + let shape = RoundedRectangle(cornerRadius: 22, style: .continuous) + + content() + .padding(AppLayout.panelPadding) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .background { + shape + .fill(.ultraThinMaterial.opacity(0.65)) + .background(shape.fill(Color.white.opacity(0.04))) + .overlay( + shape.stroke(Color.white.opacity(0.28), lineWidth: 2) + ) + .overlay( + shape.stroke( + LinearGradient( + colors: [ + Color.cyan.opacity(0.45), + Color.white.opacity(0.12), + Color.blue.opacity(0.25) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1 + ) + ) + .shadow(color: Color.black.opacity(0.35), radius: 28, x: 0, y: 16) + } + .padding(AppLayout.screenInset) + } +} + +struct GlassFooterBar: View { + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .frame(maxWidth: .infinity) + .frame(minHeight: AppLayout.footerMinHeight) + .padding(.horizontal, 8) + .padding(.vertical, 16) + .background(alignment: .top) { + Rectangle() + .fill(Color.white.opacity(0.12)) + .frame(height: 1) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + } + } +} + +struct GlassPrimaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 17, weight: .semibold, design: .rounded)) + .foregroundStyle(.white) + .padding(.horizontal, 28) + .padding(.vertical, 14) + .background { + Capsule() + .fill(AppTheme.accent) + .overlay( + Capsule() + .fill(Color.black.opacity(configuration.isPressed ? 0.15 : 0)) + ) + .overlay( + Capsule() + .stroke(Color.white.opacity(0.35), lineWidth: 1) + ) + .shadow(color: Color.cyan.opacity(0.35), radius: configuration.isPressed ? 6 : 14, x: 0, y: configuration.isPressed ? 2 : 8) + } + .scaleEffect(configuration.isPressed ? 0.97 : 1) + .animation(.easeOut(duration: 0.15), value: configuration.isPressed) + } +} + +struct AccentGradientText: ViewModifier { + func body(content: Content) -> some View { + content + .foregroundStyle(AppTheme.accent) + } +} + +extension View { + func glassCard( + cornerRadius: CGFloat = 20, + tint: Color = .white, + borderWidth: CGFloat = 1.5, + borderTint: Color? = nil + ) -> some View { + modifier(GlassCardModifier( + cornerRadius: cornerRadius, + tint: tint, + borderWidth: borderWidth, + borderTint: borderTint + )) + } + + func appGlassBackground() -> some View { + background { + GlassBackground() + } + } + + func accentGradientForeground() -> some View { + modifier(AccentGradientText()) + } +} + +// MARK: - Components + +struct GlassSectionHeader: View { + let title: String + var subtitle: String? = nil + + var body: some View { + VStack(spacing: 6) { + Text(title) + .font(.system(size: 28, weight: .bold, design: .rounded)) + .accentGradientForeground() + + if let subtitle { + Text(subtitle) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(.white.opacity(0.65)) + .multilineTextAlignment(.center) + } + } + } +} + +struct GlassIconBadge: View { + let systemName: String + var size: CGFloat = 72 + + var body: some View { + ZStack { + Circle() + .fill(.ultraThinMaterial) + .frame(width: size + 24, height: size + 24) + .overlay( + Circle() + .stroke(Color.white.opacity(0.35), lineWidth: 1) + ) + .shadow(color: Color.cyan.opacity(0.2), radius: 16, x: 0, y: 8) + + Image(systemName: systemName) + .font(.system(size: size * 0.45, weight: .light)) + .foregroundStyle(AppTheme.accent) + } + } +} + +struct GlassWeightDisplay: View { + let value: String + let unit: String + var large: Bool = false + + var body: some View { + VStack(spacing: 8) { + Text(value) + .font(.system(size: large ? 56 : 40, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + .shadow(color: Color.cyan.opacity(0.5), radius: 12, x: 0, y: 0) + + Text(unit) + .font(.system(size: large ? 18 : 14, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.6)) + .textCase(.uppercase) + .tracking(1.2) + } + .padding(.horizontal, 36) + .padding(.vertical, 24) + .glassCard(cornerRadius: 24, tint: .cyan) + } +} diff --git a/TrackWeight/HomeView.swift b/TrackWeight/HomeView.swift index 5dadf96..eb5ee65 100644 --- a/TrackWeight/HomeView.swift +++ b/TrackWeight/HomeView.swift @@ -7,127 +7,131 @@ import SwiftUI struct HomeView: View { let onBegin: () -> Void - + var body: some View { - VStack(spacing: 40) { - Spacer() - - // Title section - VStack(spacing: 15) { - Image(systemName: "scalemass") - .font(.system(size: 80, weight: .ultraLight)) - .foregroundStyle(Color.blue) - - Text("TrackWeight") - .font(.system(size: 48, weight: .bold, design: .rounded)) - .foregroundStyle( - LinearGradient( - colors: [.blue, .teal, .cyan], - startPoint: .leading, - endPoint: .trailing - ) - ) - } - - // Description section - VStack(spacing: 20) { - Text("Transform your MacBook trackpad into a precision scale using Apple's private MultitouchSupport framework to read pressure values with gram-level accuracy.") - .font(.system(size: 18, weight: .medium)) - .foregroundStyle(Color.primary) - .multilineTextAlignment(.center) - .frame(maxWidth: 550) - - // Limitations section - VStack(spacing: 12) { - Text("Important Limitations") - .font(.system(size: 16, weight: .semibold)) - .foregroundStyle(Color.orange) - - VStack(spacing: 8) { - LimitationRow( - icon: "hand.point.up.left", - text: "Requires finger contact for capacitive detection" - ) - LimitationRow( - icon: "chart.line.downtrend.xyaxis", - text: "May experience pressure drift when placing objects" - ) - LimitationRow( - icon: "cube.fill", - text: "Metal/magnetic objects may not work" - ) + ZStack { + GlassBackground() + + GlassScreenFrame { + VStack(spacing: 0) { + headerSection + .padding(.bottom, AppLayout.sectionSpacing) + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: AppLayout.sectionSpacing) { + descriptionSection + limitationsSection + } + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + GlassFooterBar { + Button(action: onBegin) { + HStack(spacing: 10) { + Text("Begin") + Image(systemName: "arrow.right") + .font(.system(size: 15, weight: .semibold)) + } + } + .buttonStyle(GlassPrimaryButtonStyle()) } + .padding(.top, 8) } - .padding(.horizontal, 30) - .padding(.vertical, 20) - .background( - RoundedRectangle(cornerRadius: 15) - .foregroundColor(Color.orange.opacity(0.05)) - .overlay( - RoundedRectangle(cornerRadius: 15) - .stroke(Color.orange.opacity(0.2), lineWidth: 1) - ) - ) - .frame(maxWidth: 500) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } - - Spacer() - - // Begin button - Button(action: onBegin) { - HStack(spacing: 10) { - Text("Begin") - .font(.system(size: 18, weight: .semibold)) - Image(systemName: "arrow.right") - .font(.system(size: 16, weight: .semibold)) - } - .foregroundStyle(Color.white) - .frame(width: 140, height: 50) - .background( - RoundedRectangle(cornerRadius: 25) - .fill( - LinearGradient( - colors: [.blue, .teal], - startPoint: .leading, - endPoint: .trailing - ) - ) - .shadow(color: .blue.opacity(0.3), radius: 10, x: 0, y: 5) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + private var headerSection: some View { + VStack(spacing: 16) { + GlassIconBadge(systemName: "scalemass", size: 72) + + Text("TrackWeight") + .font(.system(size: 44, weight: .bold, design: .rounded)) + .accentGradientForeground() + .multilineTextAlignment(.center) + + Text("Precision scale for your MacBook trackpad") + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.55)) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + } + + private var descriptionSection: some View { + Text("Transform your MacBook trackpad into a precision scale with gram-level pressure readings.") + .font(.system(size: 16, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.85)) + .multilineTextAlignment(.center) + .lineSpacing(4) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + .padding(.horizontal, 4) + .padding(.vertical, 20) + .glassCard(cornerRadius: 16, tint: .cyan, borderTint: .cyan) + } + + private var limitationsSection: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Important Limitations") + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundStyle( + LinearGradient( + colors: [Color.orange, Color(red: 1, green: 0.75, blue: 0.4)], + startPoint: .leading, + endPoint: .trailing + ) ) - } - .buttonStyle(.plain) - .scaleEffect(1.0) - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: true) - .padding(.vertical, 10) + .frame(maxWidth: .infinity, alignment: .leading) - Spacer() + VStack(alignment: .leading, spacing: 12) { + LimitationRow( + icon: "hand.point.up.left", + text: "Requires finger contact for capacitive detection" + ) + LimitationRow( + icon: "chart.line.downtrend.xyaxis", + text: "May experience pressure drift when placing objects" + ) + LimitationRow( + icon: "cube.fill", + text: "Metal or magnetic objects may not work reliably" + ) + } } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(.horizontal, 40) + .padding(22) + .frame(maxWidth: .infinity, alignment: .leading) + .glassCard(cornerRadius: 18, tint: .orange, borderWidth: 2, borderTint: .orange) } } struct LimitationRow: View { let icon: String let text: String - + var body: some View { - HStack(spacing: 12) { + HStack(alignment: .top, spacing: 14) { Image(systemName: icon) - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(Color.orange) - .frame(width: 20) - + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(Color.orange.opacity(0.95)) + .frame(width: 22, alignment: .center) + .padding(.top, 2) + Text(text) - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(Color.secondary) + .font(.system(size: 14, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.78)) .multilineTextAlignment(.leading) - - Spacer() + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) } } } #Preview { HomeView(onBegin: {}) + .frame(width: 760, height: 600) + .preferredColorScheme(.dark) } diff --git a/TrackWeight/ScaleView.swift b/TrackWeight/ScaleView.swift index 97d9fb5..6ac142d 100644 --- a/TrackWeight/ScaleView.swift +++ b/TrackWeight/ScaleView.swift @@ -9,110 +9,69 @@ struct ScaleView: View { @StateObject private var viewModel = ScaleViewModel() @State private var scaleCompression: CGFloat = 0 @State private var displayShake = false - @State private var particleOffset: CGFloat = 0 @State private var keyMonitor: Any? - + var body: some View { GeometryReader { geometry in - ZStack { - // Animated gradient background -// LinearGradient( -// colors: [ -// Color(red: 0.95, green: 0.97, blue: 1.0), -// Color(red: 0.85, green: 0.92, blue: 0.98) -// ], -// startPoint: .topLeading, -// endPoint: .bottomTrailing -// ) -// .ignoresSafeArea() - - VStack(spacing: geometry.size.height * 0.06) { - // Title with subtitle directly underneath + let scaleFactor = min(geometry.size.width / 760, geometry.size.height / 560) + + GlassScreenFrame { + VStack(spacing: 0) { VStack(spacing: 8) { Text("Track Weight") - .font(.system(size: min(max(geometry.size.width * 0.05, 24), 42), weight: .bold, design: .rounded)) - .foregroundStyle( - LinearGradient( - colors: [.blue, .teal, .cyan], - startPoint: .leading, - endPoint: .trailing - ) - ) - .minimumScaleFactor(0.7) - .lineLimit(1) - + .font(.system(size: min(max(28, geometry.size.width * 0.04), 36), weight: .bold, design: .rounded)) + .accentGradientForeground() + .multilineTextAlignment(.center) + Text("Place your finger on the trackpad to begin") - .font(.system(size: min(max(geometry.size.width * 0.022, 14), 18), weight: .medium)) - .foregroundStyle(.gray) + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.55)) .multilineTextAlignment(.center) - .frame(maxWidth: geometry.size.width * 0.8) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) .opacity(viewModel.hasTouch ? 0 : 1) .animation(.easeInOut(duration: 0.5), value: viewModel.hasTouch) } - .frame(height: max(geometry.size.height * 0.15, 80)) // Fixed height for title + subtitle - .frame(maxWidth: .infinity) // Ensure full width for centering - - Spacer() - - // Cartoon Digital Scale - responsive size - HStack { - Spacer() - CartoonScaleView( - weight: viewModel.currentWeight, - hasTouch: viewModel.hasTouch, - compression: $scaleCompression, - displayShake: $displayShake, - scaleFactor: min(geometry.size.width / 700, geometry.size.height / 500) - ) - Spacer() - } - - Spacer() - - // Fixed container for button to prevent jumping - VStack(spacing: 10) { - if viewModel.hasTouch { - Text("Press spacebar or click to zero") - .font(.system(size: min(max(geometry.size.width * 0.018, 12), 16), weight: .medium)) - .foregroundStyle(.gray) - } - - Button(action: { - viewModel.zeroScale() - }) { - HStack(spacing: 8) { - Image(systemName: "arrow.clockwise") - .font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold)) - Text("Zero Scale") - .font(.system(size: min(max(geometry.size.width * 0.02, 14), 18), weight: .semibold)) + .frame(maxWidth: .infinity) + .padding(.bottom, 16) + + Spacer(minLength: 12) + + CartoonScaleView( + weight: viewModel.currentWeight, + hasTouch: viewModel.hasTouch, + compression: $scaleCompression, + displayShake: $displayShake, + scaleFactor: scaleFactor + ) + + Spacer(minLength: 12) + + GlassFooterBar { + VStack(spacing: 10) { + if viewModel.hasTouch { + Text("Press spacebar or click to zero") + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.5)) } - .foregroundStyle(.white) - .frame(width: min(max(geometry.size.width * 0.2, 140), 180), - height: min(max(geometry.size.height * 0.08, 40), 55)) - .background( - RoundedRectangle(cornerRadius: 25) - .fill( - LinearGradient( - colors: [.blue, .teal], - startPoint: .leading, - endPoint: .trailing - ) - ) - ) + + Button(action: { viewModel.zeroScale() }) { + HStack(spacing: 8) { + Image(systemName: "arrow.clockwise") + Text("Zero Scale") + } + } + .buttonStyle(GlassPrimaryButtonStyle()) + .opacity(viewModel.hasTouch ? 1 : 0) + .scaleEffect(viewModel.hasTouch ? 1 : 0.85) + .animation(.spring(response: 0.4, dampingFraction: 0.8), value: viewModel.hasTouch) } - .buttonStyle(.plain) - .opacity(viewModel.hasTouch ? 1 : 0) - .scaleEffect(viewModel.hasTouch ? 1 : 0.8) - .animation(.spring(response: 0.4, dampingFraction: 0.8), value: viewModel.hasTouch) } - .frame(height: min(max(geometry.size.height * 0.15, 80), 100)) // Fixed space for button + instruction - .frame(maxWidth: .infinity) // Ensure full width for centering } - .padding(.horizontal, max(geometry.size.width * 0.05, 20)) - .padding(.vertical, max(geometry.size.height * 0.03, 20)) - .frame(maxWidth: .infinity, maxHeight: .infinity) // Ensure the VStack takes full available space + .frame(maxWidth: .infinity, maxHeight: .infinity) } } + .frame(maxWidth: .infinity, maxHeight: .infinity) .focusable() .modifier(FocusEffectModifier()) .onChange(of: viewModel.currentWeight) { newWeight in @@ -129,17 +88,16 @@ struct ScaleView: View { removeKeyMonitoring() } } - + private func setupKeyMonitoring() { keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - // Space key code is 49 if event.keyCode == 49 && viewModel.hasTouch { viewModel.zeroScale() } return event } } - + private func removeKeyMonitoring() { if let monitor = keyMonitor { NSEvent.removeMonitor(monitor) @@ -154,122 +112,134 @@ struct CartoonScaleView: View { @Binding var compression: CGFloat @Binding var displayShake: Bool let scaleFactor: CGFloat - + var body: some View { VStack(spacing: 0) { - // Scale platform (top) - responsive to weight - RoundedRectangle(cornerRadius: 8) - .fill( - LinearGradient( - colors: [.gray.opacity(0.3), .gray.opacity(0.6)], - startPoint: .top, - endPoint: .bottom - ) + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .stroke(Color.white.opacity(0.4), lineWidth: 1) ) - .frame(width: 200 * scaleFactor, height: 12 * scaleFactor) + .frame(width: 200 * scaleFactor, height: 14 * scaleFactor) .offset(y: compression * 15) - - // Scale body + .shadow(color: Color.cyan.opacity(0.2), radius: 8, x: 0, y: 4) + ZStack { - // Main body - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill(.ultraThinMaterial) + .background( + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill( + LinearGradient( + colors: [ + Color.white.opacity(0.18), + Color.cyan.opacity(0.08) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 24, style: .continuous) + .stroke( + LinearGradient( + colors: [ + Color.white.opacity(0.6), + Color.white.opacity(0.15) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1.5 + ) + ) + .frame(width: 260 * scaleFactor, height: 160 * scaleFactor) + .shadow(color: Color.black.opacity(0.35), radius: 24, x: 0, y: 14) + + RoundedRectangle(cornerRadius: 14, style: .continuous) .fill( LinearGradient( colors: [ - Color(red: 0.95, green: 0.95, blue: 0.97), - Color(red: 0.85, green: 0.85, blue: 0.90) + Color(red: 0.05, green: 0.12, blue: 0.22).opacity(0.9), + Color(red: 0.08, green: 0.28, blue: 0.35).opacity(0.85) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) - .frame(width: 250 * scaleFactor, height: 150 * scaleFactor) - .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 8) - - // Display screen - RoundedRectangle(cornerRadius: 12) - .fill(.black) - .frame(width: 180 * scaleFactor, height: 60 * scaleFactor) - .offset(y: -10) .overlay( - RoundedRectangle(cornerRadius: 12) - .fill( - LinearGradient( - colors: [.teal.opacity(0.8), .blue.opacity(0.6)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .frame(width: 176 * scaleFactor, height: 56 * scaleFactor) - .offset(y: -10) + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(Color.cyan.opacity(0.45), lineWidth: 1) ) - - // Weight display + .frame(width: 188 * scaleFactor, height: 64 * scaleFactor) + .offset(y: -12) + .shadow(color: Color.cyan.opacity(hasTouch ? 0.5 : 0.15), radius: hasTouch ? 12 : 4) + VStack(spacing: 2) { Text(String(format: "%.1f", weight)) - .font(.system(size: 32 * scaleFactor, weight: .bold, design: .monospaced)) + .font(.system(size: 34 * scaleFactor, weight: .bold, design: .rounded)) .foregroundStyle(.white) - .shadow(color: .teal, radius: hasTouch ? 2 : 0) - .animation(.easeInOut(duration: 0.2), value: weight) - + .shadow(color: Color.cyan.opacity(0.8), radius: hasTouch ? 8 : 0) + Text("grams") - .font(.system(size: 12 * scaleFactor, weight: .medium)) - .foregroundStyle(.white.opacity(0.8)) + .font(.system(size: 11 * scaleFactor, weight: .semibold, design: .rounded)) + .foregroundStyle(.white.opacity(0.55)) + .tracking(0.8) } - .offset(y: -10) - - // Status indicator - simple and clean + .offset(y: -12) + if hasTouch { Circle() - .fill(.teal) + .fill(Color.cyan) .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) - .offset(x: 90 * scaleFactor, y: -50 * scaleFactor) + .shadow(color: .cyan, radius: 6) + .offset(x: 95 * scaleFactor, y: -52 * scaleFactor) } - - // Fun face on the scale - positioned below the display screen + VStack(spacing: 8 * scaleFactor) { - // Eyes HStack(spacing: 15 * scaleFactor) { Circle() - .fill(.black) + .fill(.white.opacity(0.85)) .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) Circle() - .fill(.black) + .fill(.white.opacity(0.85)) .frame(width: 8 * scaleFactor, height: 8 * scaleFactor) } - - // Responsive mouth expression + Group { if hasTouch && weight > 5 { - // Happy mouth when weighing something substantial Path { path in path.move(to: CGPoint(x: 0, y: 0)) path.addQuadCurve(to: CGPoint(x: 20, y: 0), control: CGPoint(x: 0, y: 15)) } - .stroke(.black, lineWidth: 2 * scaleFactor) + .stroke(Color.white.opacity(0.8), lineWidth: 2 * scaleFactor) .frame(width: 20 * scaleFactor, height: 10 * scaleFactor) } else { - // Neutral mouth - Rectangle() - .fill(.black) - .frame(width: 12 * scaleFactor, height: 2 * scaleFactor) + Capsule() + .fill(.white.opacity(0.5)) + .frame(width: 14 * scaleFactor, height: 2.5 * scaleFactor) } } .animation(.easeInOut(duration: 0.3), value: weight > 5) } - .offset(y: 60 * scaleFactor) // Position well below the display screen + .offset(y: 58 * scaleFactor) } - - // Scale legs + HStack(spacing: 140 * scaleFactor) { ForEach(0..<2, id: \.self) { _ in - RoundedRectangle(cornerRadius: 4) - .fill(.gray.opacity(0.7)) - .frame(width: 12 * scaleFactor, height: 25 * scaleFactor) + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 6, style: .continuous) + .stroke(Color.white.opacity(0.25), lineWidth: 0.5) + ) + .frame(width: 14 * scaleFactor, height: 28 * scaleFactor) .offset(y: compression * 3) } } - .offset(y: -5) + .offset(y: -6) } .animation(.spring(response: 0.4, dampingFraction: 0.8), value: compression) } @@ -286,5 +256,9 @@ struct FocusEffectModifier: ViewModifier { } #Preview { - ScaleView() + ZStack { + GlassBackground() + ScaleView() + } + .frame(width: 700, height: 500) } diff --git a/TrackWeight/SettingsView.swift b/TrackWeight/SettingsView.swift index 67e7a25..5ce28fd 100644 --- a/TrackWeight/SettingsView.swift +++ b/TrackWeight/SettingsView.swift @@ -9,105 +9,110 @@ import SwiftUI struct SettingsView: View { @StateObject private var viewModel = ContentViewModel() @State private var showDebugView = false - + var body: some View { - VStack(spacing: 0) { - // Minimal Header - Text("Settings") - .font(.title) - .fontWeight(.medium) - .padding(.top, 32) - .padding(.bottom, 32) - - // Settings Cards - VStack(spacing: 20) { - // Device Card - SettingsCard { - VStack(spacing: 20) { - // Status Row - HStack { - HStack(spacing: 12) { - Text("Trackpad") - .font(.headline) - .fontWeight(.medium) - } - - Spacer() - - if !viewModel.availableDevices.isEmpty { - Text("\(viewModel.availableDevices.count) device\(viewModel.availableDevices.count == 1 ? "" : "s")") - .font(.caption) - .foregroundColor(.secondary) - } - } - - // Device Selector - if !viewModel.availableDevices.isEmpty { - HStack { - Picker("", selection: Binding( - get: { viewModel.selectedDevice }, - set: { device in - if let device = device { - viewModel.selectDevice(device) + GlassScreenFrame { + VStack(spacing: 0) { + GlassSectionHeader(title: "Settings", subtitle: "Trackpad & diagnostics") + .padding(.bottom, AppLayout.sectionSpacing) + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 18) { + SettingsCard { + VStack(spacing: 20) { + HStack { + Label { + Text("Trackpad") + .font(.headline.weight(.semibold)) + .foregroundStyle(.white.opacity(0.95)) + } icon: { + Image(systemName: "rectangle.and.hand.point.up.left") + .foregroundStyle(AppTheme.accent) + } + + Spacer() + + if !viewModel.availableDevices.isEmpty { + Text("\(viewModel.availableDevices.count) device\(viewModel.availableDevices.count == 1 ? "" : "s")") + .font(.caption.weight(.medium)) + .foregroundStyle(.white.opacity(0.5)) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(Capsule().fill(.white.opacity(0.08))) + } + } + + if !viewModel.availableDevices.isEmpty { + HStack { + Picker("", selection: Binding( + get: { viewModel.selectedDevice }, + set: { device in + if let device = device { + viewModel.selectDevice(device) + } + } + )) { + ForEach(viewModel.availableDevices, id: \.self) { device in + Text(device.deviceName) + .tag(device as OMSDeviceInfo?) + } } + .pickerStyle(.menu) + .tint(.white) + + Spacer() } - )) { - ForEach(viewModel.availableDevices, id: \.self) { device in - Text(device.deviceName) - .tag(device as OMSDeviceInfo?) + } else { + HStack { + Text("No devices available") + .foregroundStyle(.white.opacity(0.5)) + Spacer() } } - .pickerStyle(MenuPickerStyle()) - - Spacer() - } - } else { - HStack { - Text("No devices available") - .foregroundColor(.secondary) - Spacer() } } - } - } - - // Debug Card - SettingsCard { - Button(action: { showDebugView = true }) { - HStack(spacing: 16) { - VStack(alignment: .leading, spacing: 4) { - Text("Debug Console") - .font(.headline) - .fontWeight(.medium) - .foregroundColor(.primary) - - Text("Raw touch data & diagnostics") - .font(.caption) - .foregroundColor(.secondary) + + SettingsCard { + Button(action: { showDebugView = true }) { + HStack(spacing: 16) { + GlassIconBadge(systemName: "terminal", size: 44) + + VStack(alignment: .leading, spacing: 4) { + Text("Debug Console") + .font(.headline.weight(.semibold)) + .foregroundStyle(.white.opacity(0.95)) + + Text("Raw touch data & diagnostics") + .font(.caption) + .foregroundStyle(.white.opacity(0.55)) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundStyle(.white.opacity(0.4)) + } + .contentShape(Rectangle()) } - - Spacer() - - Image(systemName: "chevron.right") - .font(.caption) - .fontWeight(.medium) - .foregroundColor(.secondary.opacity(0.6)) + .buttonStyle(CardButtonStyle()) } - .contentShape(Rectangle()) } - .buttonStyle(CardButtonStyle()) + .frame(maxWidth: 480) + .frame(maxWidth: .infinity) } + .frame(maxWidth: .infinity, maxHeight: .infinity) } - .frame(maxWidth: 480) - .padding(.horizontal, 40) - - Spacer() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(NSColor.windowBackgroundColor)) .sheet(isPresented: $showDebugView) { DebugView() - .frame(minWidth: 700, minHeight: 500) + .frame( + minWidth: AppLayout.windowMinWidth, + minHeight: AppLayout.windowMinHeight + ) + .preferredColorScheme(.dark) } .onAppear { viewModel.loadDevices() @@ -117,20 +122,16 @@ struct SettingsView: View { struct SettingsCard: View { let content: Content - + init(@ViewBuilder content: () -> Content) { self.content = content() } - + var body: some View { - VStack { - content - } - .padding(24) - .background(Color(NSColor.controlBackgroundColor)) - .cornerRadius(16) - .shadow(color: Color.black.opacity(0.03), radius: 1, x: 0, y: 1) - .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 4) + content + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + .glassCard(cornerRadius: 18, borderWidth: 1.5) } } @@ -138,10 +139,15 @@ struct CardButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.98 : 1.0) - .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + .opacity(configuration.isPressed ? 0.92 : 1) + .animation(.easeInOut(duration: 0.12), value: configuration.isPressed) } } #Preview { - SettingsView() -} \ No newline at end of file + ZStack { + GlassBackground() + SettingsView() + } + .frame(width: 760, height: 600) +} diff --git a/TrackWeight/TrackWeightApp.swift b/TrackWeight/TrackWeightApp.swift index 6ff16f1..266ac1d 100644 --- a/TrackWeight/TrackWeightApp.swift +++ b/TrackWeight/TrackWeightApp.swift @@ -15,6 +15,10 @@ struct TrackWeightApp: App { WindowGroup { ContentView() } + .defaultSize( + width: AppLayout.defaultWidth, + height: AppLayout.defaultHeight + ) } } diff --git a/TrackWeight/TrackWeightView.swift b/TrackWeight/TrackWeightView.swift index 6908f95..6fb74cb 100644 --- a/TrackWeight/TrackWeightView.swift +++ b/TrackWeight/TrackWeightView.swift @@ -7,151 +7,139 @@ import SwiftUI struct TrackWeightView: View { @StateObject private var viewModel = WeighingViewModel() - + var body: some View { - VStack(spacing: 30) { - switch viewModel.state { - case .welcome: - WelcomeView { - viewModel.startWeighing() - } - - case .waitingForFinger: - FingerTimerView( - progress: viewModel.fingerTimer, - hasDetectedFinger: viewModel.fingerTimer > 0 - ) - - case .waitingForItem: - InstructionView( - title: "Place your item", - subtitle: "While maintaining contact with the trackpad, gently place your item. Use as little pressure as possible with your reference finger.", - disclaimer: "Keep your finger still and apply minimal pressure", - icon: "cube.box" - ) - - case .weighing: - WeighingView( - currentPressure: viewModel.currentPressure, - isStabilizing: viewModel.isStabilizing, - stabilityProgress: viewModel.stabilityProgress - ) - - case .result(let weight): - ResultView(weight: weight) { - viewModel.restart() + GlassScreenFrame { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 24) { + switch viewModel.state { + case .welcome: + WelcomeView { + viewModel.startWeighing() + } + + case .waitingForFinger: + FingerTimerView( + progress: viewModel.fingerTimer, + hasDetectedFinger: viewModel.fingerTimer > 0 + ) + + case .waitingForItem: + InstructionView( + title: "Place your item", + subtitle: "While maintaining contact with the trackpad, gently place your item. Use as little pressure as possible with your reference finger.", + disclaimer: "Keep your finger still and apply minimal pressure", + icon: "cube.box" + ) + + case .weighing: + WeighingView( + currentPressure: viewModel.currentPressure, + isStabilizing: viewModel.isStabilizing, + stabilityProgress: viewModel.stabilityProgress + ) + + case .result(let weight): + ResultView(weight: weight) { + viewModel.restart() + } + } } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) } + .frame(maxWidth: .infinity, maxHeight: .infinity) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(.windowBackgroundColor)) - .animation(.easeInOut(duration: 0.6), value: viewModel.state) + .animation(.easeInOut(duration: 0.5), value: viewModel.state) } } struct WelcomeView: View { let onStart: () -> Void - + var body: some View { - VStack(spacing: 25) { - Image(systemName: "scalemass") - .font(.system(size: 80, weight: .ultraLight)) - .foregroundStyle(.primary) - - Text("TrackWeight") - .font(.system(size: 36, weight: .bold, design: .rounded)) - .foregroundStyle(.primary) - + VStack(spacing: 28) { + GlassIconBadge(systemName: "scalemass", size: 72) + + GlassSectionHeader( + title: "Guided Weigh", + subtitle: "Step-by-step precision on your trackpad" + ) + Text("Turn your trackpad into a precision scale. Place objects and get their weight in grams.") - .font(.system(size: 16, weight: .medium)) - .foregroundStyle(.secondary) + .font(.system(size: 16, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.7)) .multilineTextAlignment(.center) .frame(maxWidth: 400) - - VStack(spacing: 15) { - Button(action: onStart) { - Text("Begin") - .font(.system(size: 16, weight: .semibold)) - .foregroundStyle(.white) - .frame(width: 120, height: 40) - .background( - RoundedRectangle(cornerRadius: 20) - .fill(.blue) - ) - } - .buttonStyle(.plain) + + Button(action: onStart) { + Text("Start") } + .buttonStyle(GlassPrimaryButtonStyle()) } + .padding(32) + .frame(maxWidth: .infinity) + .glassCard(cornerRadius: 28) } } struct FingerTimerView: View { let progress: Float let hasDetectedFinger: Bool - + var body: some View { - VStack(spacing: 30) { - Image(systemName: "hand.point.up.left") - .font(.system(size: 60, weight: .light)) - .foregroundStyle(.blue) - - Text("Hold your finger steady") - .font(.system(size: 28, weight: .bold, design: .rounded)) - .foregroundStyle(.primary) - - VStack(spacing: 8) { - Text("Keep your finger on the trackpad for 3 seconds") - .font(.system(size: 16, weight: .medium)) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 300) - - Text("This establishes your baseline pressure") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.tertiary) - .multilineTextAlignment(.center) - .frame(maxWidth: 300) - } - - // Bubble filling animation + VStack(spacing: 28) { + GlassIconBadge(systemName: "hand.point.up.left", size: 56) + + GlassSectionHeader( + title: "Hold your finger steady", + subtitle: "Keep contact for 3 seconds to set baseline" + ) + ZStack { Circle() - .stroke(.blue.opacity(0.3), lineWidth: 4) - .frame(width: 100, height: 100) - + .stroke(Color.white.opacity(0.15), lineWidth: 6) + .frame(width: 120, height: 120) + Circle() - .fill(.blue.opacity(0.2)) - .frame(width: 100, height: 100) - + .fill(Color.cyan.opacity(0.08)) + .frame(width: 120, height: 120) + if hasDetectedFinger { Circle() .trim(from: 0, to: CGFloat(progress)) - .stroke(.blue, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .frame(width: 100, height: 100) + .stroke( + AppTheme.accent, + style: StrokeStyle(lineWidth: 6, lineCap: .round) + ) + .frame(width: 120, height: 120) .rotationEffect(.degrees(-90)) .animation(.linear(duration: 0.1), value: progress) - - // Gentle bubble effect + Circle() .fill( RadialGradient( - colors: [.blue.opacity(0.3), .blue.opacity(0.1)], + colors: [.cyan.opacity(0.35), .clear], center: .center, startRadius: 0, - endRadius: 50 + endRadius: 55 ) ) - .frame(width: CGFloat(progress) * 80 + 20, height: CGFloat(progress) * 80 + 20) + .frame(width: CGFloat(progress) * 90 + 24, height: CGFloat(progress) * 90 + 24) .animation(.easeInOut(duration: 0.2), value: progress) - + Text("\(Int((1 - progress) * 3) + 1)") - .font(.system(size: 24, weight: .bold, design: .monospaced)) - .foregroundStyle(.blue) + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundStyle(.white) } } - .scaleEffect(hasDetectedFinger ? 1.0 : 0.8) - .animation(.spring(response: 0.3, dampingFraction: 0.8), value: hasDetectedFinger) + .scaleEffect(hasDetectedFinger ? 1 : 0.88) + .animation(.spring(response: 0.35, dampingFraction: 0.8), value: hasDetectedFinger) } + .padding(36) + .frame(maxWidth: .infinity) + .glassCard(cornerRadius: 28) } } @@ -160,40 +148,49 @@ struct InstructionView: View { let subtitle: String let disclaimer: String? let icon: String - + init(title: String, subtitle: String, disclaimer: String? = nil, icon: String) { self.title = title self.subtitle = subtitle self.disclaimer = disclaimer self.icon = icon } - + var body: some View { - VStack(spacing: 20) { - Image(systemName: icon) - .font(.system(size: 60, weight: .light)) - .foregroundStyle(.blue) - + VStack(spacing: 22) { + GlassIconBadge(systemName: icon, size: 52) + Text(title) - .font(.system(size: 28, weight: .bold, design: .rounded)) - .foregroundStyle(.primary) - - VStack(spacing: 10) { + .font(.system(size: 26, weight: .bold, design: .rounded)) + .accentGradientForeground() + + VStack(spacing: 12) { Text(subtitle) - .font(.system(size: 16, weight: .medium)) - .foregroundStyle(.secondary) + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.72)) .multilineTextAlignment(.center) - .frame(maxWidth: 350) - - if let disclaimer = disclaimer { + .lineSpacing(3) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity) + + if let disclaimer { Text(disclaimer) - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.orange) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(Color.orange.opacity(0.9)) .multilineTextAlignment(.center) - .frame(maxWidth: 300) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background( + Capsule() + .fill(Color.orange.opacity(0.12)) + .overlay(Capsule().stroke(Color.orange.opacity(0.3), lineWidth: 1)) + ) } } } + .padding(32) + .frame(maxWidth: .infinity) + .glassCard(cornerRadius: 28) } } @@ -201,116 +198,106 @@ struct WeighingView: View { let currentPressure: Float let isStabilizing: Bool let stabilityProgress: Float - + var body: some View { - VStack(spacing: 30) { - Text("Weighing...") - .font(.system(size: 24, weight: .semibold, design: .rounded)) - .foregroundStyle(.primary) - - VStack(spacing: 10) { - Text(String(format: "%.1f", currentPressure)) - .font(.system(size: 64, weight: .bold, design: .monospaced)) - .foregroundStyle(.blue) - .animation(.easeInOut(duration: 0.2), value: currentPressure) - - Text("grams") - .font(.system(size: 20, weight: .medium)) - .foregroundStyle(.secondary) - } - - VStack(spacing: 12) { + VStack(spacing: 24) { + Text("Weighing…") + .font(.system(size: 22, weight: .semibold, design: .rounded)) + .foregroundStyle(.white.opacity(0.9)) + + GlassWeightDisplay( + value: String(format: "%.1f", currentPressure), + unit: "grams", + large: true + ) + .animation(.easeInOut(duration: 0.2), value: currentPressure) + + VStack(spacing: 14) { Text("Release pressure while maintaining contact") - .font(.system(size: 16, weight: .semibold)) - .foregroundStyle(.orange) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundStyle(Color.orange.opacity(0.95)) .multilineTextAlignment(.center) - + Text("Keep your finger on the trackpad but apply as little pressure as possible") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.secondary) + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.55)) .multilineTextAlignment(.center) - .frame(maxWidth: 350) - + .frame(maxWidth: 340) + if isStabilizing { - VStack(spacing: 8) { - Text("Stabilizing...") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.orange) - - // Progress bar - ZStack { - RoundedRectangle(cornerRadius: 4) - .fill(.orange.opacity(0.2)) - .frame(width: 200, height: 8) - - HStack { - RoundedRectangle(cornerRadius: 4) - .fill(.orange) - .frame(width: 200 * CGFloat(stabilityProgress), height: 8) - .animation(.linear(duration: 0.1), value: stabilityProgress) - - Spacer() - } + VStack(spacing: 10) { + Text("Stabilizing…") + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(Color.orange.opacity(0.9)) + + ZStack(alignment: .leading) { + Capsule() + .fill(Color.white.opacity(0.1)) + .frame(width: 220, height: 8) + + Capsule() + .fill(AppTheme.warmAccent) + .frame(width: 220 * CGFloat(stabilityProgress), height: 8) + .animation(.linear(duration: 0.1), value: stabilityProgress) } - .frame(width: 200) - + Text("\(Int((1 - stabilityProgress) * 2) + 1)s remaining") - .font(.system(size: 12, weight: .medium, design: .monospaced)) - .foregroundStyle(.orange.opacity(0.8)) + .font(.system(size: 11, weight: .medium, design: .monospaced)) + .foregroundStyle(.white.opacity(0.45)) } + .padding(16) + .glassCard(cornerRadius: 14, tint: .orange) } } - .frame(maxWidth: 350) } } } - struct ResultView: View { let weight: Float let onRestart: () -> Void - + var body: some View { - VStack(spacing: 30) { + VStack(spacing: 28) { Image(systemName: "checkmark.circle.fill") - .font(.system(size: 60)) - .foregroundStyle(.green) - .scaleEffect(1.0) - .onAppear { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - // Animation handled by parent view - } - } - + .font(.system(size: 56)) + .foregroundStyle( + LinearGradient( + colors: [.green, .mint], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .shadow(color: .green.opacity(0.4), radius: 12) + Text("Your object weighs") - .font(.system(size: 20, weight: .medium)) - .foregroundStyle(.secondary) - - VStack(spacing: 5) { - Text(String(format: "%.1f", weight)) - .font(.system(size: 56, weight: .bold, design: .monospaced)) - .foregroundStyle(.primary) - - Text("grams") - .font(.system(size: 18, weight: .medium)) - .foregroundStyle(.secondary) - } - + .font(.system(size: 18, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.65)) + + GlassWeightDisplay( + value: String(format: "%.1f", weight), + unit: "grams", + large: true + ) + Button(action: onRestart) { - Image(systemName: "arrow.clockwise") - .font(.system(size: 20, weight: .medium)) - .foregroundStyle(.blue) - .frame(width: 44, height: 44) - .background( - Circle() - .fill(.blue.opacity(0.1)) - ) + HStack(spacing: 8) { + Image(systemName: "arrow.clockwise") + Text("Weigh again") + } } - .buttonStyle(.plain) + .buttonStyle(GlassPrimaryButtonStyle()) } + .padding(36) + .frame(maxWidth: .infinity) + .glassCard(cornerRadius: 28, tint: .green, borderTint: .green) } } #Preview { - TrackWeightView() + ZStack { + GlassBackground() + TrackWeightView() + } + .frame(width: 700, height: 500) }