mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Onboarding Enhancement
This commit is contained in:
parent
08adf8d1c4
commit
fd5042390d
10 changed files with 854 additions and 52 deletions
|
|
@ -447,7 +447,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
if change.newValue == true && Defaults[.sneakPeekStyles] != .standard {
|
||||
Defaults[.sneakPeekStyles] = .standard
|
||||
}
|
||||
self?.debouncedUpdateWindowSize()
|
||||
// Update window size IMMEDIATELY (no debouncing) to prevent position shift
|
||||
self?.updateWindowSizeIfNeeded()
|
||||
}.store(in: &cancellables)
|
||||
|
||||
// Observe screen recording settings changes
|
||||
|
|
|
|||
|
|
@ -312,6 +312,10 @@
|
|||
},
|
||||
"Choose your preferred AI model and configuration" : {
|
||||
|
||||
},
|
||||
"Choose Your Profile" : {
|
||||
"comment" : "A title displayed above the list of profile selection options.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Circle" : {
|
||||
|
||||
|
|
@ -819,9 +823,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Media change with horizontal gestures" : {
|
||||
|
||||
},
|
||||
"Media controls" : {
|
||||
|
||||
|
|
@ -1114,6 +1115,10 @@
|
|||
"comment" : "The title of the privacy and security settings screen.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Privacy Policy" : {
|
||||
"comment" : "A link to the privacy policy.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Progress bar" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
|
@ -1264,6 +1269,10 @@
|
|||
},
|
||||
"Select Calendars" : {
|
||||
|
||||
},
|
||||
"Select one or more profiles to customize your experience" : {
|
||||
"comment" : "A description below the header that instructs users to select one or more profiles to customize their experience.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Select the music source you want to use. You can change this later in the app settings." : {
|
||||
|
||||
|
|
@ -1519,8 +1528,9 @@
|
|||
"Try different search terms" : {
|
||||
|
||||
},
|
||||
"Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : {
|
||||
|
||||
"Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled. Two-finger horizontal swipes to control media playback" : {
|
||||
"comment" : "A footer text describing the gesture controls in the settings view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"UI Mode" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,19 @@ struct OnboardingFinishView: View {
|
|||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
// Privacy Policy Link
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://ebullioscopic.github.io/DynamicIsland/privacy-policy") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Text("Privacy Policy")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ enum OnboardingStep {
|
|||
case cameraPermission
|
||||
case calendarPermission
|
||||
case musicPermission
|
||||
case profileSelection
|
||||
case finished
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +82,17 @@ struct OnboardingView: View {
|
|||
case .musicPermission:
|
||||
MusicControllerSelectionView(
|
||||
onContinue: {
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .profileSelection
|
||||
}
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
case .profileSelection:
|
||||
ProfileSelectionView(
|
||||
onContinue: { profiles in
|
||||
applyProfileSettings(profiles)
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .finished
|
||||
}
|
||||
|
|
@ -105,3 +117,4 @@ struct OnboardingView: View {
|
|||
await calendarService.requestAccess()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
260
DynamicIsland/components/Onboarding/ProfileSelectionView.swift
Normal file
260
DynamicIsland/components/Onboarding/ProfileSelectionView.swift
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
//
|
||||
// ProfileSelectionView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created on 2025-10-05.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
struct UserProfile: Identifiable, Hashable {
|
||||
let id: String
|
||||
let name: String
|
||||
let icon: String
|
||||
let description: String
|
||||
let gradient: [Color]
|
||||
}
|
||||
|
||||
struct ProfileSelectionView: View {
|
||||
@State private var selectedProfiles: Set<String> = []
|
||||
let onContinue: (Set<String>) -> Void
|
||||
|
||||
let profiles: [UserProfile] = [
|
||||
UserProfile(
|
||||
id: "developer",
|
||||
name: "Developer",
|
||||
icon: "terminal.fill",
|
||||
description: "Code and debug with color picker, stats monitoring, and screen assistant.",
|
||||
gradient: [Color.blue, Color.purple]
|
||||
),
|
||||
UserProfile(
|
||||
id: "designer",
|
||||
name: "Designer",
|
||||
icon: "paintbrush.fill",
|
||||
description: "Create and design with color picker, mirror, and visual effects.",
|
||||
gradient: [Color.pink, Color.orange]
|
||||
),
|
||||
UserProfile(
|
||||
id: "lightuse",
|
||||
name: "Light Use",
|
||||
icon: "sparkles",
|
||||
description: "Simple and minimal interface with just the essentials for everyday tasks.",
|
||||
gradient: [Color.green, Color.mint]
|
||||
),
|
||||
UserProfile(
|
||||
id: "student",
|
||||
name: "Student",
|
||||
icon: "book.fill",
|
||||
description: "Stay organized with calendar, timer, and battery monitoring.",
|
||||
gradient: [Color.indigo, Color.cyan]
|
||||
)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 24) {
|
||||
// Header
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "person.3.fill")
|
||||
.font(.system(size: 50))
|
||||
.foregroundStyle(.linearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Choose Your Profile")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Select one or more profiles to customize your experience")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
// Profile Grid
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
|
||||
ForEach(profiles) { profile in
|
||||
ProfileCard(
|
||||
profile: profile,
|
||||
isSelected: selectedProfiles.contains(profile.id),
|
||||
onTap: {
|
||||
toggleProfile(profile.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Continue Button
|
||||
Button(action: {
|
||||
if !selectedProfiles.isEmpty {
|
||||
onContinue(selectedProfiles)
|
||||
}
|
||||
}) {
|
||||
Text("Continue")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(selectedProfiles.isEmpty)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(
|
||||
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
}
|
||||
|
||||
private func toggleProfile(_ profileId: String) {
|
||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||
if selectedProfiles.contains(profileId) {
|
||||
selectedProfiles.remove(profileId)
|
||||
} else {
|
||||
selectedProfiles.insert(profileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileCard: View {
|
||||
let profile: UserProfile
|
||||
let isSelected: Bool
|
||||
let onTap: () -> Void
|
||||
|
||||
@State private var isHovering = false
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: profile.icon)
|
||||
.font(.system(size: 28))
|
||||
.foregroundStyle(.linearGradient(colors: profile.gradient, startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
|
||||
Spacer()
|
||||
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
.foregroundStyle(.linearGradient(colors: profile.gradient, startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.transition(.scale.combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
|
||||
Text(profile.name)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(profile.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(height: 160)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(isSelected ?
|
||||
LinearGradient(colors: profile.gradient.map { $0.opacity(0.15) }, startPoint: .topLeading, endPoint: .bottomTrailing) :
|
||||
LinearGradient(colors: [Color.gray.opacity(0.1)], startPoint: .topLeading, endPoint: .bottomTrailing)
|
||||
)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.strokeBorder(
|
||||
isSelected ?
|
||||
LinearGradient(colors: profile.gradient, startPoint: .topLeading, endPoint: .bottomTrailing) :
|
||||
LinearGradient(colors: [Color.clear], startPoint: .topLeading, endPoint: .bottomTrailing),
|
||||
lineWidth: isSelected ? 2 : 0
|
||||
)
|
||||
)
|
||||
.scaleEffect(isHovering ? 1.02 : 1.0)
|
||||
.shadow(color: isSelected ? profile.gradient[0].opacity(0.3) : .clear, radius: 8, x: 0, y: 4)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hovering in
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
isHovering = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Profile Settings Configuration
|
||||
|
||||
func applyProfileSettings(_ profiles: Set<String>) {
|
||||
// Clipboard is ALWAYS enabled (per user request)
|
||||
Defaults[.enableClipboardManager] = true
|
||||
|
||||
// Developer Profile Settings
|
||||
let isDeveloper = profiles.contains("developer")
|
||||
if isDeveloper {
|
||||
Defaults[.enableColorPickerFeature] = true
|
||||
Defaults[.enableStatsFeature] = true
|
||||
Defaults[.enableTimerFeature] = true
|
||||
Defaults[.enableScreenAssistant] = true
|
||||
Defaults[.showMirror] = false
|
||||
Defaults[.enableMinimalisticUI] = false
|
||||
}
|
||||
|
||||
// Designer Profile Settings
|
||||
let isDesigner = profiles.contains("designer")
|
||||
if isDesigner {
|
||||
Defaults[.enableColorPickerFeature] = true
|
||||
Defaults[.showMirror] = true
|
||||
Defaults[.lightingEffect] = true
|
||||
Defaults[.inlineHUD] = true
|
||||
Defaults[.enableStatsFeature] = false
|
||||
Defaults[.enableTimerFeature] = false
|
||||
Defaults[.enableMinimalisticUI] = false
|
||||
Defaults[.enableScreenAssistant] = false
|
||||
}
|
||||
|
||||
// Light Use Profile Settings
|
||||
let isLightUse = profiles.contains("lightuse")
|
||||
if isLightUse {
|
||||
Defaults[.enableMinimalisticUI] = true
|
||||
Defaults[.enableColorPickerFeature] = false
|
||||
Defaults[.showMirror] = false
|
||||
Defaults[.enableStatsFeature] = false
|
||||
Defaults[.enableTimerFeature] = true
|
||||
Defaults[.inlineHUD] = false
|
||||
Defaults[.enableScreenAssistant] = false
|
||||
}
|
||||
|
||||
// Student Profile Settings
|
||||
let isStudent = profiles.contains("student")
|
||||
if isStudent {
|
||||
Defaults[.enableTimerFeature] = true
|
||||
Defaults[.showCalendar] = true
|
||||
Defaults[.enableColorPickerFeature] = false
|
||||
Defaults[.showMirror] = false
|
||||
Defaults[.enableStatsFeature] = false
|
||||
Defaults[.enableMinimalisticUI] = false
|
||||
Defaults[.enableScreenAssistant] = false
|
||||
}
|
||||
|
||||
// If Light Use is NOT selected but others are, ensure minimalistic is OFF
|
||||
if !isLightUse && !profiles.isEmpty {
|
||||
Defaults[.enableMinimalisticUI] = false
|
||||
}
|
||||
|
||||
// Common settings for all profiles
|
||||
Defaults[.menubarIcon] = true
|
||||
Defaults[.enableHaptics] = true
|
||||
|
||||
print("✅ Applied profile settings for: \(profiles.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProfileSelectionView(onContinue: { profiles in
|
||||
print("Selected profiles: \(profiles)")
|
||||
})
|
||||
}
|
||||
|
|
@ -58,6 +58,19 @@ struct WelcomeView: View {
|
|||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
|
||||
// Privacy Policy Link
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://ebullioscopic.github.io/DynamicIsland/privacy-policy") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Text("Privacy Policy")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ extension Defaults.Keys {
|
|||
// MARK: Gestures
|
||||
static let enableGestures = Key<Bool>("enableGestures", default: true)
|
||||
static let closeGestureEnabled = Key<Bool>("closeGestureEnabled", default: true)
|
||||
static let enableMediaGestures = Key<Bool>("enableMediaGestures", default: true)
|
||||
static let gestureSensitivity = Key<CGFloat>("gestureSensitivity", default: 200.0)
|
||||
|
||||
// MARK: Media playback
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue