general: cleanup new page
This commit is contained in:
@@ -7,163 +7,372 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct OnboardingVisualEffectView: NSViewRepresentable {
|
|
||||||
let material: NSVisualEffectView.Material
|
|
||||||
let blendingMode: NSVisualEffectView.BlendingMode
|
|
||||||
|
|
||||||
func makeNSView(context: Context) -> NSVisualEffectView {
|
|
||||||
let view = NSVisualEffectView()
|
|
||||||
view.material = material
|
|
||||||
view.blendingMode = blendingMode
|
|
||||||
view.state = .active
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
|
||||||
nsView.material = material
|
|
||||||
nsView.blendingMode = blendingMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AdditionalModifiersView: View {
|
struct AdditionalModifiersView: View {
|
||||||
@Bindable var settingsManager: SettingsManager
|
@Bindable var settingsManager: SettingsManager
|
||||||
|
@State private var frontCardIndex: Int = 0
|
||||||
|
@State private var dragOffset: CGFloat = 0
|
||||||
|
@State private var isDragging: Bool = false
|
||||||
|
|
||||||
|
private let cardWidth: CGFloat = 480
|
||||||
|
private let cardHeight: CGFloat = 480
|
||||||
|
private let backCardOffset: CGFloat = 30
|
||||||
|
private let backCardScale: CGFloat = 0.92
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
VStack(spacing: 0) {
|
||||||
OnboardingVisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
SetupHeader(icon: "slider.horizontal.3", title: "Additional Options", color: .purple)
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
Text("Optional features to enhance your experience")
|
||||||
// Header
|
.font(.title3)
|
||||||
HStack {
|
.foregroundStyle(.secondary)
|
||||||
Text("Additional Modifiers")
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Card stack
|
||||||
|
ZStack {
|
||||||
|
// Card 0 (Enforce Mode)
|
||||||
|
cardView(for: 0)
|
||||||
|
.zIndex(zIndex(for: 0))
|
||||||
|
.scaleEffect(scale(for: 0))
|
||||||
|
.offset(x: xOffset(for: 0), y: yOffset(for: 0))
|
||||||
|
.opacity(opacity(for: 0))
|
||||||
|
|
||||||
|
// Card 1 (Smart Mode)
|
||||||
|
cardView(for: 1)
|
||||||
|
.zIndex(zIndex(for: 1))
|
||||||
|
.scaleEffect(scale(for: 1))
|
||||||
|
.offset(x: xOffset(for: 1), y: yOffset(for: 1))
|
||||||
|
.opacity(opacity(for: 1))
|
||||||
|
}
|
||||||
|
.gesture(dragGesture)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Navigation controls
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
Button(action: { swapCards() }) {
|
||||||
|
Image(systemName: "chevron.left")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.semibold)
|
.frame(width: 44, height: 44)
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 40)
|
.buttonStyle(.plain)
|
||||||
.padding(.top, 20)
|
.glassEffectIfAvailable(GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||||
|
.disabled(frontCardIndex == 0)
|
||||||
|
.opacity(frontCardIndex == 0 ? 0.4 : 1)
|
||||||
|
|
||||||
Spacer()
|
// Page indicators with labels
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
cardIndicator(index: 0, icon: "video.fill", label: "Enforce")
|
||||||
|
cardIndicator(index: 1, icon: "brain.fill", label: "Smart")
|
||||||
|
}
|
||||||
|
|
||||||
// Main content area with stacking effect
|
Button(action: { swapCards() }) {
|
||||||
HStack(spacing: 0) {
|
Image(systemName: "chevron.right")
|
||||||
// Smart Mode Card (stacked behind, 50% width, offset 10% from right)
|
.font(.title2)
|
||||||
ZStack {
|
.frame(width: 44, height: 44)
|
||||||
// Background card with shadow and rounded corners
|
}
|
||||||
RoundedRectangle(cornerRadius: 16)
|
.buttonStyle(.plain)
|
||||||
.fill(Color(NSColor.windowBackgroundColor).opacity(0.8))
|
.glassEffectIfAvailable(GlassStyle.regular.interactive(), in: .rect(cornerRadius: 10))
|
||||||
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
.disabled(frontCardIndex == 1)
|
||||||
.frame(width: 500, height: 500) // 50% of 1000px width
|
.opacity(frontCardIndex == 1 ? 0.4 : 1)
|
||||||
.offset(x: 100) // 10% offset from right (1000 * 0.1 = 100)
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
// Smart mode content
|
}
|
||||||
SmartModeSetupView(settingsManager: settingsManager)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding(20)
|
.padding()
|
||||||
}
|
.background(.clear)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
}
|
||||||
.zIndex(0)
|
|
||||||
|
// MARK: - Card Indicator
|
||||||
// Enforce Mode Card (in front)
|
|
||||||
VStack(spacing: 24) {
|
@ViewBuilder
|
||||||
SetupHeader(icon: "video.fill", title: "Enforce Mode", color: .accentColor)
|
private func cardIndicator(index: Int, icon: String, label: String) -> some View {
|
||||||
|
Button(action: {
|
||||||
Text("Use your camera to ensure you take breaks")
|
if index != frontCardIndex {
|
||||||
.font(.title3)
|
swapCards()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.caption)
|
||||||
|
Text(label)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.foregroundStyle(index == frontCardIndex ? .primary : .secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.glassEffectIfAvailable(
|
||||||
|
index == frontCardIndex
|
||||||
|
? GlassStyle.regular.tint(Color.accentColor.opacity(0.3))
|
||||||
|
: GlassStyle.regular,
|
||||||
|
in: .capsule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Card Transform Calculations
|
||||||
|
|
||||||
|
private func zIndex(for cardIndex: Int) -> Double {
|
||||||
|
let isFront = cardIndex == frontCardIndex
|
||||||
|
let dragProgress = abs(dragOffset) / 150
|
||||||
|
|
||||||
|
if isDragging && dragProgress > 0.3 {
|
||||||
|
return isFront ? 0 : 1
|
||||||
|
}
|
||||||
|
return isFront ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scale(for cardIndex: Int) -> CGFloat {
|
||||||
|
let isFront = cardIndex == frontCardIndex
|
||||||
|
let dragProgress = min(abs(dragOffset) / 150, 1.0)
|
||||||
|
|
||||||
|
if isFront {
|
||||||
|
return 1.0 - (dragProgress * (1.0 - backCardScale))
|
||||||
|
} else {
|
||||||
|
return backCardScale + (dragProgress * (1.0 - backCardScale))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func xOffset(for cardIndex: Int) -> CGFloat {
|
||||||
|
let isFront = cardIndex == frontCardIndex
|
||||||
|
let dragProgress = min(abs(dragOffset) / 150, 1.0)
|
||||||
|
let backPeekX = backCardOffset
|
||||||
|
|
||||||
|
if isFront {
|
||||||
|
return dragOffset + (dragProgress * backPeekX * (dragOffset > 0 ? -1 : 1))
|
||||||
|
} else {
|
||||||
|
return backPeekX * (1.0 - dragProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func yOffset(for cardIndex: Int) -> CGFloat {
|
||||||
|
let isFront = cardIndex == frontCardIndex
|
||||||
|
let dragProgress = min(abs(dragOffset) / 150, 1.0)
|
||||||
|
let backPeekY: CGFloat = 15
|
||||||
|
|
||||||
|
if isFront {
|
||||||
|
return dragProgress * backPeekY
|
||||||
|
} else {
|
||||||
|
return backPeekY * (1.0 - dragProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func opacity(for cardIndex: Int) -> CGFloat {
|
||||||
|
let isFront = cardIndex == frontCardIndex
|
||||||
|
let dragProgress = min(abs(dragOffset) / 150, 1.0)
|
||||||
|
|
||||||
|
if isFront {
|
||||||
|
return 1.0 - (dragProgress * 0.3)
|
||||||
|
} else {
|
||||||
|
return 0.7 + (dragProgress * 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Card Views
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func cardView(for index: Int) -> some View {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 16)
|
||||||
|
.fill(Color(NSColor.windowBackgroundColor).opacity(0.8))
|
||||||
|
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 4)
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if index == 0 {
|
||||||
|
enforceModeContent
|
||||||
|
} else {
|
||||||
|
smartModeContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
}
|
||||||
|
.frame(width: cardWidth, height: cardHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var enforceModeContent: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "video.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
|
||||||
|
Text("Enforce Mode")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Text("Use your camera to ensure you take breaks")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Enable Enforce Mode")
|
||||||
|
.font(.headline)
|
||||||
|
Text("Camera activates before lookaway reminders")
|
||||||
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
}
|
||||||
.padding(.bottom, 10)
|
Spacer()
|
||||||
|
Toggle("", isOn: $settingsManager.settings.enforcementMode)
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Camera Access")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
VStack(spacing: 20) {
|
if CameraAccessService.shared.isCameraAuthorized {
|
||||||
HStack {
|
Label("Authorized", systemImage: "checkmark.circle.fill")
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
.font(.caption)
|
||||||
Text("Enable Enforce Mode")
|
.foregroundStyle(.green)
|
||||||
.font(.headline)
|
} else if let error = CameraAccessService.shared.cameraError {
|
||||||
Text("Camera activates 3 seconds before lookaway reminders")
|
Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.orange)
|
||||||
}
|
} else {
|
||||||
Spacer()
|
Label("Not authorized", systemImage: "xmark.circle.fill")
|
||||||
Toggle(
|
.font(.caption)
|
||||||
"",
|
.foregroundStyle(.secondary)
|
||||||
isOn: Binding(
|
|
||||||
get: {
|
|
||||||
settingsManager.settings.enforcementMode
|
|
||||||
},
|
|
||||||
set: { newValue in
|
|
||||||
print("🎛️ Toggle changed to: \(newValue)")
|
|
||||||
settingsManager.settings.enforcementMode = newValue
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
|
|
||||||
// Camera access status display
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Camera Access")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
if CameraAccessService.shared.isCameraAuthorized {
|
|
||||||
Label("Authorized", systemImage: "checkmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
} else if let error = CameraAccessService.shared.cameraError {
|
|
||||||
Label(error.localizedDescription, systemImage: "exclamationmark.triangle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
} else {
|
|
||||||
Label("Not authorized", systemImage: "xmark.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if !CameraAccessService.shared.isCameraAuthorized {
|
|
||||||
Button("Request Access") {
|
|
||||||
print("📷 Request Access button clicked")
|
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
|
||||||
try await CameraAccessService.shared.requestCameraAccess()
|
|
||||||
print("✓ Camera access granted via button")
|
|
||||||
} catch {
|
|
||||||
print("⚠️ Camera access failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 500) // 50% width
|
|
||||||
.zIndex(1)
|
Spacer()
|
||||||
|
|
||||||
|
if !CameraAccessService.shared.isCameraAuthorized {
|
||||||
|
Button("Request Access") {
|
||||||
|
Task { @MainActor in
|
||||||
|
do {
|
||||||
|
try await CameraAccessService.shared.requestCameraAccess()
|
||||||
|
} catch {
|
||||||
|
print("Camera access failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding()
|
||||||
Spacer()
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var smartModeContent: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "brain.fill")
|
||||||
|
.font(.system(size: 40))
|
||||||
|
.foregroundStyle(.purple)
|
||||||
|
|
||||||
|
Text("Smart Mode")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
|
||||||
|
Text("Automatically manage timers based on activity")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
smartModeToggle(
|
||||||
|
icon: "arrow.up.left.and.arrow.down.right",
|
||||||
|
iconColor: .blue,
|
||||||
|
title: "Auto-pause on Fullscreen",
|
||||||
|
subtitle: "Pause during videos, games, presentations",
|
||||||
|
isOn: $settingsManager.settings.smartMode.autoPauseOnFullscreen
|
||||||
|
)
|
||||||
|
|
||||||
|
smartModeToggle(
|
||||||
|
icon: "moon.zzz.fill",
|
||||||
|
iconColor: .indigo,
|
||||||
|
title: "Auto-pause on Idle",
|
||||||
|
subtitle: "Pause when you're inactive",
|
||||||
|
isOn: $settingsManager.settings.smartMode.autoPauseOnIdle
|
||||||
|
)
|
||||||
|
|
||||||
|
smartModeToggle(
|
||||||
|
icon: "chart.line.uptrend.xyaxis",
|
||||||
|
iconColor: .green,
|
||||||
|
title: "Track Usage Statistics",
|
||||||
|
subtitle: "Monitor active and idle time",
|
||||||
|
isOn: $settingsManager.settings.smartMode.trackUsage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func smartModeToggle(icon: String, iconColor: Color, title: String, subtitle: String, isOn: Binding<Bool>) -> some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundStyle(iconColor)
|
||||||
|
.frame(width: 24)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Toggle("", isOn: isOn)
|
||||||
|
.labelsHidden()
|
||||||
|
.controlSize(.small)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Gestures & Navigation
|
||||||
|
|
||||||
|
private var dragGesture: some Gesture {
|
||||||
|
DragGesture()
|
||||||
|
.onChanged { value in
|
||||||
|
isDragging = true
|
||||||
|
dragOffset = value.translation.width
|
||||||
|
}
|
||||||
|
.onEnded { value in
|
||||||
|
let threshold: CGFloat = 80
|
||||||
|
let shouldSwap = abs(value.translation.width) > threshold ||
|
||||||
|
abs(value.predictedEndTranslation.width) > 150
|
||||||
|
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||||
|
if shouldSwap {
|
||||||
|
frontCardIndex = 1 - frontCardIndex
|
||||||
|
}
|
||||||
|
dragOffset = 0
|
||||||
|
isDragging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func swapCards() {
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
|
||||||
|
frontCardIndex = 1 - frontCardIndex
|
||||||
}
|
}
|
||||||
.frame(
|
|
||||||
minWidth: 1000,
|
|
||||||
minHeight: {
|
|
||||||
#if APPSTORE
|
|
||||||
return 700
|
|
||||||
#else
|
|
||||||
return 1000
|
|
||||||
#endif
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Additional Modifiers View") {
|
#Preview("Additional Modifiers View") {
|
||||||
AdditionalModifiersView(settingsManager: SettingsManager.shared)
|
AdditionalModifiersView(settingsManager: SettingsManager.shared)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,13 +153,13 @@ struct OnboardingContainerView: View {
|
|||||||
.tag(4)
|
.tag(4)
|
||||||
.tabItem { Image(systemName: "figure.stand") }
|
.tabItem { Image(systemName: "figure.stand") }
|
||||||
|
|
||||||
GeneralSetupView(settingsManager: settingsManager, isOnboarding: true)
|
|
||||||
.tag(5)
|
|
||||||
.tabItem { Image(systemName: "gearshape.fill") }
|
|
||||||
|
|
||||||
AdditionalModifiersView(settingsManager: settingsManager)
|
AdditionalModifiersView(settingsManager: settingsManager)
|
||||||
|
.tag(5)
|
||||||
|
.tabItem { Image(systemName: "slider.horizontal.3") }
|
||||||
|
|
||||||
|
GeneralSetupView(settingsManager: settingsManager, isOnboarding: true)
|
||||||
.tag(6)
|
.tag(6)
|
||||||
.tabItem { Image(systemName: "plus.circle.fill") }
|
.tabItem { Image(systemName: "gearshape.fill") }
|
||||||
|
|
||||||
CompletionView()
|
CompletionView()
|
||||||
.tag(7)
|
.tag(7)
|
||||||
|
|||||||
Reference in New Issue
Block a user