mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
Applied latest changes from boring.notch
This commit is contained in:
parent
13a2ff0e20
commit
bad481588b
5 changed files with 453 additions and 0 deletions
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// MusicControllerSelectionView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Alexander on 2025-06-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
|
||||
|
||||
struct MusicControllerSelectionView: View {
|
||||
let onContinue: () -> Void
|
||||
|
||||
@Default(.mediaController) var mediaController
|
||||
|
||||
private var availableMediaControllers: [MediaControllerType] {
|
||||
if MusicManager.shared.isNowPlayingDeprecated {
|
||||
return MediaControllerType.allCases.filter { $0 != .nowPlaying }
|
||||
} else {
|
||||
return MediaControllerType.allCases
|
||||
}
|
||||
}
|
||||
|
||||
@State private var selectedMediaController: MediaControllerType = Defaults[.mediaController]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text("Choose a Music Source")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.padding(.top, 24)
|
||||
|
||||
Text("Select the music source you want to use. You can change this later in the app settings.")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal)
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(availableMediaControllers) { controller in
|
||||
ControllerOptionView(
|
||||
controller: controller,
|
||||
isSelected: self.selectedMediaController == controller
|
||||
)
|
||||
.onTapGesture {
|
||||
self.selectedMediaController = controller
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Continue", action: {
|
||||
self.mediaController = self.selectedMediaController
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.Name.mediaControllerChanged,
|
||||
object: nil
|
||||
)
|
||||
onContinue()
|
||||
})
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(
|
||||
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ControllerOptionView: View {
|
||||
let controller: MediaControllerType
|
||||
let isSelected: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.title2)
|
||||
.foregroundColor(isSelected ? .accentColor : .secondary.opacity(0.5))
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: isSelected)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(controller.rawValue)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text(controller.description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if controller == .youtubeMusic, let url = URL(string: "https://github.com/th-ch/youtube-music") {
|
||||
Link("View on GitHub: th-ch/youtube-music", destination: url)
|
||||
.font(.subheadline)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(isSelected ? Color.accentColor.opacity(0.15) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(isSelected ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1.5)
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MediaControllerType {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .nowPlaying:
|
||||
return "Works with most media apps, including browsers, to detect what's playing. Note: This may be removed in a future macOS version."
|
||||
case .spotify:
|
||||
return "Connects directly to the Spotify app."
|
||||
case .appleMusic:
|
||||
return "Connects directly to the Apple Music app."
|
||||
case .youtubeMusic:
|
||||
return "Requires a third-party client with API plugin enabled."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MusicControllerSelectionView(onContinue: {})
|
||||
.frame(width: 400, height: 600)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// OnboardingFinishView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Alexander on 2025-06-23.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OnboardingFinishView: View {
|
||||
let onFinish: () -> Void
|
||||
let onOpenSettings: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.accentColor)
|
||||
.padding()
|
||||
|
||||
Text("You're All Set!")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("You can now enjoy the app. If you want to tweak things further, you can always visit the settings.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Button(action: onOpenSettings) {
|
||||
Label("Customize in Settings", systemImage: "gear")
|
||||
.controlSize(.large)
|
||||
}
|
||||
.controlSize(.large)
|
||||
|
||||
Button("Finish", action: onFinish)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(
|
||||
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
OnboardingFinishView(onFinish: { }, onOpenSettings: { })
|
||||
}
|
||||
107
DynamicIsland/components/Onboarding/OnboardingView.swift
Normal file
107
DynamicIsland/components/Onboarding/OnboardingView.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// OnboardingView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Alexander on 2025-06-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
|
||||
enum OnboardingStep {
|
||||
case welcome
|
||||
case cameraPermission
|
||||
case calendarPermission
|
||||
case musicPermission
|
||||
case finished
|
||||
}
|
||||
|
||||
private let calendarService = CalendarService()
|
||||
|
||||
struct OnboardingView: View {
|
||||
@State private var step: OnboardingStep = .welcome
|
||||
let onFinish: () -> Void
|
||||
let onOpenSettings: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
switch step {
|
||||
case .welcome:
|
||||
WelcomeView {
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .cameraPermission
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
|
||||
case .cameraPermission:
|
||||
PermissionRequestView(
|
||||
icon: Image(systemName: "camera.fill"),
|
||||
title: "Enable Camera Access",
|
||||
description: "Dynamic Island includes a mirror feature that lets you quickly check your appearance using your camera, right from the notch. Camera access is required only to show this live preview. You can turn the mirror feature on or off at any time in the app.",
|
||||
privacyNote: "Your camera is never used without your consent, and nothing is recorded or stored.",
|
||||
onAllow: {
|
||||
Task {
|
||||
await requestCameraPermission()
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .calendarPermission
|
||||
}
|
||||
}
|
||||
},
|
||||
onSkip: {
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .calendarPermission
|
||||
}
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
case .calendarPermission:
|
||||
PermissionRequestView(
|
||||
icon: Image(systemName: "calendar"),
|
||||
title: "Enable Calendar Access",
|
||||
description: "Dynamic Island can show all your upcoming events in one place. Access to your calendar is needed to display your schedule.",
|
||||
privacyNote: "Your calendar data is only used to show your events and is never shared.",
|
||||
onAllow: {
|
||||
Task {
|
||||
await requestCalendarPermission()
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .musicPermission
|
||||
}
|
||||
}
|
||||
},
|
||||
onSkip: {
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .musicPermission
|
||||
}
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
case .musicPermission:
|
||||
MusicControllerSelectionView(
|
||||
onContinue: {
|
||||
withAnimation(.easeInOut(duration: 0.6)) {
|
||||
step = .finished
|
||||
}
|
||||
}
|
||||
)
|
||||
.transition(.opacity)
|
||||
|
||||
case .finished:
|
||||
OnboardingFinishView(onFinish: onFinish, onOpenSettings: onOpenSettings)
|
||||
}
|
||||
}
|
||||
.frame(width: 400, height: 600)
|
||||
}
|
||||
|
||||
// MARK: - Permission Request Logic
|
||||
|
||||
func requestCameraPermission() async {
|
||||
await AVCaptureDevice.requestAccess(for: .video)
|
||||
}
|
||||
|
||||
func requestCalendarPermission() async {
|
||||
await calendarService.requestAccess()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// PermissionsRequestView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Alexander on 2025-06-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PermissionRequestView: View {
|
||||
let icon: Image
|
||||
let title: String
|
||||
let description: String
|
||||
let privacyNote: String?
|
||||
let onAllow: () -> Void
|
||||
let onSkip: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 28) {
|
||||
icon
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 70, height: 56)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.top, 32)
|
||||
|
||||
Text(title)
|
||||
.font(.title)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text(description)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
|
||||
if let privacyNote = privacyNote {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "lock.shield")
|
||||
.foregroundColor(.secondary)
|
||||
Text(privacyNote)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button("Not Now") { onSkip() }
|
||||
.buttonStyle(.bordered)
|
||||
Button("Allow Access") { onAllow() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding(.top, 10)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
.background(
|
||||
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
}
|
||||
}
|
||||
85
DynamicIsland/components/Onboarding/WelcomeView.swift
Normal file
85
DynamicIsland/components/Onboarding/WelcomeView.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// WelcomeView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Created by Richard Kunkli on 2024. 09. 26..
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
struct WelcomeView: View {
|
||||
var onGetStarted: (() -> Void)? = nil
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
ZStack {
|
||||
Image("spotlight")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.bottom)
|
||||
.blur(radius: 3)
|
||||
.offset(y: -5)
|
||||
.background(SparkleView().opacity(0.6))
|
||||
VStack(spacing: 8) {
|
||||
Image("logo2")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 100, height: 100)
|
||||
.padding(.bottom, 8)
|
||||
Text("Dynamic Island")
|
||||
.font(.system(.largeTitle, design: .default))
|
||||
.fontWeight(.semibold)
|
||||
Text("Welcome")
|
||||
.font(.title)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
if false {
|
||||
Text("PRO")
|
||||
.font(.system(size: 18, design: .rounded))
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(LinearGradient(colors: [.white.opacity(0.7), .white.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.strokeBorder(LinearGradient(stops: [.init(color: .white.opacity(0.7), location: 0.3), .init(color: .clear, location: 0.6)], startPoint: .topLeading, endPoint: .bottomTrailing))
|
||||
.blendMode(.overlay)
|
||||
)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
|
||||
|
||||
Button {
|
||||
onGetStarted?()
|
||||
} label: {
|
||||
Text("Get started")
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.buttonStyle(BorderedProminentButtonStyle())
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
Image("theboringteam")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 22)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
.padding()
|
||||
.padding(.bottom, 36)
|
||||
.blendMode(.overlay)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.ignoresSafeArea()
|
||||
.background {
|
||||
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
WelcomeView()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue