mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
commit
37a81a17bd
6 changed files with 337 additions and 77 deletions
|
|
@ -250,9 +250,15 @@
|
||||||
},
|
},
|
||||||
"Check for Updates…" : {
|
"Check for Updates…" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Choose a Music Source" : {
|
"Choose a Music Source" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Choose File" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Circle" : {
|
"Circle" : {
|
||||||
|
|
||||||
|
|
@ -299,12 +305,18 @@
|
||||||
},
|
},
|
||||||
"CPU Usage" : {
|
"CPU Usage" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Current custom timer: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Current History" : {
|
"Current History" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Current Items" : {
|
"Current Items" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Custom" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Custom color" : {
|
"Custom color" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|
@ -325,9 +337,18 @@
|
||||||
},
|
},
|
||||||
"Custom notch size - %.0f" : {
|
"Custom notch size - %.0f" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Custom Timer" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Custom Timer Duration" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Custom vizualizers (Lottie)" : {
|
"Custom vizualizers (Lottie)" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Custom: %@" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Customize in Settings" : {
|
"Customize in Settings" : {
|
||||||
|
|
||||||
|
|
@ -335,7 +356,7 @@
|
||||||
"Default" : {
|
"Default" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Demo (10s)" : {
|
"Default: dynamic.m4a" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Description" : {
|
"Description" : {
|
||||||
|
|
@ -428,12 +449,18 @@
|
||||||
},
|
},
|
||||||
"Enable music live activity" : {
|
"Enable music live activity" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enable sneak peek" : {
|
"Enable sneak peek" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enable stats monitoring in Settings to view system performance data." : {
|
"Enable stats monitoring in Settings to view system performance data." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Enable timer feature in Settings to use this tab" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enable timer live activity" : {
|
"Enable timer live activity" : {
|
||||||
|
|
||||||
|
|
@ -649,6 +676,9 @@
|
||||||
},
|
},
|
||||||
"Minimum hover duration" : {
|
"Minimum hover duration" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Minutes" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Mirror" : {
|
"Mirror" : {
|
||||||
|
|
||||||
|
|
@ -824,6 +854,9 @@
|
||||||
},
|
},
|
||||||
"Remember last tab" : {
|
"Remember last tab" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Reset to Default" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Restart" : {
|
"Restart" : {
|
||||||
|
|
||||||
|
|
@ -847,6 +880,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Seconds" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Select Calendars" : {
|
"Select Calendars" : {
|
||||||
|
|
||||||
|
|
@ -859,6 +895,9 @@
|
||||||
},
|
},
|
||||||
"Selected animation" : {
|
"Selected animation" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Settings" : {
|
"Settings" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -956,9 +995,6 @@
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Start 5-min Demo Timer" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Start Demo Timer:" : {
|
"Start Demo Timer:" : {
|
||||||
|
|
||||||
|
|
@ -980,9 +1016,6 @@
|
||||||
},
|
},
|
||||||
"Stop Monitoring" : {
|
"Stop Monitoring" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Stop Timer" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Stopped" : {
|
"Stopped" : {
|
||||||
|
|
||||||
|
|
@ -1017,6 +1050,15 @@
|
||||||
},
|
},
|
||||||
"Timer" : {
|
"Timer" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Timer Disabled" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Timer Feature" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Timer Sound" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Toggle Notch Open:" : {
|
"Toggle Notch Open:" : {
|
||||||
|
|
||||||
|
|
@ -1038,6 +1080,9 @@
|
||||||
},
|
},
|
||||||
"unmuted" : {
|
"unmuted" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Update" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Upgrade to Pro" : {
|
"Upgrade to Pro" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,39 @@ struct NotchTimerView: View {
|
||||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
@ObservedObject var timerManager = TimerManager.shared
|
@ObservedObject var timerManager = TimerManager.shared
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
|
@Default(.enableTimerFeature) var enableTimerFeature
|
||||||
|
|
||||||
|
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 32) {
|
if enableTimerFeature {
|
||||||
// Timer Progress and Controls
|
HStack(alignment: .center, spacing: 32) {
|
||||||
timerProgressSection
|
// Timer Progress and Controls
|
||||||
|
timerProgressSection
|
||||||
// Timer Control Panel
|
|
||||||
controlsSection
|
// Timer Control Panel
|
||||||
|
controlsSection
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.transition(.opacity.combined(with: .blurReplace))
|
||||||
|
} else {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "timer.slash")
|
||||||
|
.font(.system(size: 48, weight: .light))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Text("Timer Disabled")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
Text("Enable timer feature in Settings to use this tab")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.transition(.opacity.combined(with: .blurReplace))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timerProgressSection: some View {
|
private var timerProgressSection: some View {
|
||||||
|
|
@ -49,8 +71,13 @@ struct NotchTimerView: View {
|
||||||
Image(systemName: "timer")
|
Image(systemName: "timer")
|
||||||
.font(.system(size: 32, weight: .medium))
|
.font(.system(size: 32, weight: .medium))
|
||||||
.foregroundStyle(timerManager.currentColor)
|
.foregroundStyle(timerManager.currentColor)
|
||||||
.scaleEffect(timerManager.isRunning ? 1.1 : 1.0)
|
.scaleEffect(timerManager.isRunning && timerManager.isTimerActive ? 1.1 : 1.0)
|
||||||
.animation(.easeInOut(duration: 0.3).repeatForever(autoreverses: true), value: timerManager.isRunning)
|
.animation(
|
||||||
|
timerManager.isRunning && timerManager.isTimerActive ?
|
||||||
|
.easeInOut(duration: 0.3).repeatForever(autoreverses: true) :
|
||||||
|
.easeInOut(duration: 0.3),
|
||||||
|
value: timerManager.isRunning && timerManager.isTimerActive
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time display
|
// Time display
|
||||||
|
|
@ -179,41 +206,59 @@ struct NotchTimerView: View {
|
||||||
quickTimerButton(minutes: 15, title: "15 min")
|
quickTimerButton(minutes: 15, title: "15 min")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Demo timer
|
HStack(spacing: 12) {
|
||||||
Button(action: {
|
// Custom timer button
|
||||||
timerManager.startDemoTimer(duration: 10) // 10 second demo
|
Button(action: {
|
||||||
}) {
|
timerManager.startTimer(duration: customTimerDuration, name: "Custom Timer")
|
||||||
HStack(spacing: 8) {
|
}) {
|
||||||
Image(systemName: "flask")
|
VStack(spacing: 4) {
|
||||||
.font(.system(size: 14, weight: .medium))
|
Text(customTimerDisplayText)
|
||||||
Text("Demo (10s)")
|
.font(.system(size: 16, weight: .medium))
|
||||||
.font(.system(size: 16, weight: .medium))
|
Text("Custom")
|
||||||
|
.font(.system(size: 12, weight: .regular))
|
||||||
|
.opacity(0.7)
|
||||||
|
}
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(height: 60)
|
||||||
|
.frame(minWidth: 80)
|
||||||
|
.background(.white.opacity(0.08))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.stroke(.white.opacity(0.1), lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.foregroundStyle(.white)
|
.buttonStyle(PlainButtonStyle())
|
||||||
.frame(height: 40)
|
.onHover { isHovering in
|
||||||
.frame(minWidth: 120)
|
if isHovering {
|
||||||
.background(.white.opacity(0.08))
|
NSCursor.pointingHand.push()
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
} else {
|
||||||
.overlay(
|
NSCursor.pop()
|
||||||
RoundedRectangle(cornerRadius: 12)
|
}
|
||||||
.stroke(.white.opacity(0.1), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
.onHover { isHovering in
|
|
||||||
if isHovering {
|
|
||||||
NSCursor.pointingHand.push()
|
|
||||||
} else {
|
|
||||||
NSCursor.pop()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var customTimerDisplayText: String {
|
||||||
|
let totalMinutes = Int(customTimerDuration) / 60
|
||||||
|
let seconds = Int(customTimerDuration) % 60
|
||||||
|
let hours = totalMinutes / 60
|
||||||
|
let minutes = totalMinutes % 60
|
||||||
|
|
||||||
|
if hours > 0 {
|
||||||
|
return "\(hours):\(String(format: "%02d", minutes))"
|
||||||
|
} else if minutes > 0 {
|
||||||
|
return "\(minutes) min"
|
||||||
|
} else {
|
||||||
|
return "\(seconds)s"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func quickTimerButton(minutes: Int, title: String) -> some View {
|
private func quickTimerButton(minutes: Int, title: String) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
timerManager.startDemoTimer(duration: TimeInterval(minutes * 60))
|
timerManager.startTimer(duration: TimeInterval(minutes * 60), name: "\(minutes) Min Timer")
|
||||||
}) {
|
}) {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text("\(minutes)")
|
Text("\(minutes)")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import LottieUI
|
||||||
import Sparkle
|
import Sparkle
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftUIIntrospect
|
import SwiftUIIntrospect
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
||||||
|
|
@ -39,6 +40,9 @@ struct SettingsView: View {
|
||||||
NavigationLink(value: "Media") {
|
NavigationLink(value: "Media") {
|
||||||
Label("Media", systemImage: "play.laptopcomputer")
|
Label("Media", systemImage: "play.laptopcomputer")
|
||||||
}
|
}
|
||||||
|
NavigationLink(value: "Timer") {
|
||||||
|
Label("Timer", systemImage: "timer")
|
||||||
|
}
|
||||||
NavigationLink(value: "Calendar") {
|
NavigationLink(value: "Calendar") {
|
||||||
Label("Calendar", systemImage: "calendar")
|
Label("Calendar", systemImage: "calendar")
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +92,8 @@ struct SettingsView: View {
|
||||||
Appearance()
|
Appearance()
|
||||||
case "Media":
|
case "Media":
|
||||||
Media()
|
Media()
|
||||||
|
case "Timer":
|
||||||
|
TimerSettings()
|
||||||
case "Calendar":
|
case "Calendar":
|
||||||
CalendarSettings()
|
CalendarSettings()
|
||||||
case "HUD":
|
case "HUD":
|
||||||
|
|
@ -504,27 +510,6 @@ struct Media: View {
|
||||||
)
|
)
|
||||||
.animation(.easeInOut, value: coordinator.musicLiveActivityEnabled)
|
.animation(.easeInOut, value: coordinator.musicLiveActivityEnabled)
|
||||||
|
|
||||||
Toggle(
|
|
||||||
"Enable timer live activity",
|
|
||||||
isOn: $coordinator.timerLiveActivityEnabled
|
|
||||||
)
|
|
||||||
.animation(.easeInOut, value: coordinator.timerLiveActivityEnabled)
|
|
||||||
|
|
||||||
// Demo timer button
|
|
||||||
HStack {
|
|
||||||
Button("Start 5-min Demo Timer") {
|
|
||||||
TimerManager.shared.startDemoTimer(duration: 300)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button("Stop Timer") {
|
|
||||||
TimerManager.shared.stopTimer()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle("Enable sneak peek", isOn: $enableSneakPeek)
|
Toggle("Enable sneak peek", isOn: $enableSneakPeek)
|
||||||
Picker("Sneak Peek Style", selection: $sneakPeekStyles){
|
Picker("Sneak Peek Style", selection: $sneakPeekStyles){
|
||||||
ForEach(SneakPeekStyle.allCases) { style in
|
ForEach(SneakPeekStyle.allCases) { style in
|
||||||
|
|
@ -618,6 +603,157 @@ struct CalendarSettings: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TimerSettings: View {
|
||||||
|
@ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
|
@Default(.enableTimerFeature) var enableTimerFeature
|
||||||
|
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default
|
||||||
|
@State private var customMinutes: Int = 10
|
||||||
|
@State private var customSeconds: Int = 0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Defaults.Toggle("Enable timer feature", key: .enableTimerFeature)
|
||||||
|
|
||||||
|
if enableTimerFeature {
|
||||||
|
Toggle(
|
||||||
|
"Enable timer live activity",
|
||||||
|
isOn: $coordinator.timerLiveActivityEnabled
|
||||||
|
)
|
||||||
|
.animation(.easeInOut, value: coordinator.timerLiveActivityEnabled)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Timer Feature")
|
||||||
|
} footer: {
|
||||||
|
Text("Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if enableTimerFeature {
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("Custom Timer Duration")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
VStack {
|
||||||
|
Text("Minutes")
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Picker(selection: $customMinutes, label: Text("Minutes")) {
|
||||||
|
ForEach(0...59, id: \.self) { minute in
|
||||||
|
Text("\(minute)").tag(minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.frame(width: 80, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Text("Seconds")
|
||||||
|
.font(.system(size: 14, weight: .medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
Picker(selection: $customSeconds, label: Text("Seconds")) {
|
||||||
|
ForEach(0...59, id: \.self) { second in
|
||||||
|
Text("\(second)").tag(second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.frame(width: 80, height: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Current custom timer: \(customTimerDisplayText)")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Button("Update") {
|
||||||
|
customTimerDuration = Double(customMinutes * 60 + customSeconds)
|
||||||
|
}
|
||||||
|
.disabled(customMinutes == 0 && customSeconds == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Custom Timer")
|
||||||
|
} footer: {
|
||||||
|
Text("Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Timer Sound")
|
||||||
|
.font(.system(size: 16, weight: .medium))
|
||||||
|
Spacer()
|
||||||
|
Button("Choose File") {
|
||||||
|
selectCustomTimerSound()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath") {
|
||||||
|
Text("Custom: \(URL(fileURLWithPath: customTimerSoundPath).lastPathComponent)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("Default: dynamic.m4a")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("Reset to Default") {
|
||||||
|
UserDefaults.standard.removeObject(forKey: "customTimerSoundPath")
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.disabled(UserDefaults.standard.string(forKey: "customTimerSoundPath") == nil)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Timer Sound")
|
||||||
|
} footer: {
|
||||||
|
Text("Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Timer")
|
||||||
|
.onAppear {
|
||||||
|
let totalMinutes = Int(customTimerDuration) / 60
|
||||||
|
customMinutes = totalMinutes
|
||||||
|
customSeconds = Int(customTimerDuration) % 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var customTimerDisplayText: String {
|
||||||
|
let totalMinutes = Int(customTimerDuration) / 60
|
||||||
|
let seconds = Int(customTimerDuration) % 60
|
||||||
|
let hours = totalMinutes / 60
|
||||||
|
let minutes = totalMinutes % 60
|
||||||
|
|
||||||
|
if hours > 0 {
|
||||||
|
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))"
|
||||||
|
} else if minutes > 0 {
|
||||||
|
return "\(minutes):\(String(format: "%02d", seconds))"
|
||||||
|
} else {
|
||||||
|
return "\(seconds)s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectCustomTimerSound() {
|
||||||
|
let panel = NSOpenPanel()
|
||||||
|
panel.title = "Select Timer Sound"
|
||||||
|
panel.allowedContentTypes = [.audio]
|
||||||
|
panel.allowsMultipleSelection = false
|
||||||
|
panel.canChooseDirectories = false
|
||||||
|
panel.canChooseFiles = true
|
||||||
|
|
||||||
|
if panel.runModal() == .OK {
|
||||||
|
if let url = panel.url {
|
||||||
|
UserDefaults.standard.set(url.path, forKey: "customTimerSoundPath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct About: View {
|
struct About: View {
|
||||||
@State private var showBuildNumber: Bool = false
|
@State private var showBuildNumber: Bool = false
|
||||||
let updaterController: SPUStandardUpdaterController
|
let updaterController: SPUStandardUpdaterController
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
// Modified by Hariharan Mudaliar
|
// Modified by Hariharan Mudaliar
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Defaults
|
||||||
|
|
||||||
struct TabModel: Identifiable {
|
struct TabModel: Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
|
|
@ -14,19 +15,31 @@ struct TabModel: Identifiable {
|
||||||
let view: NotchViews
|
let view: NotchViews
|
||||||
}
|
}
|
||||||
|
|
||||||
let tabs = [
|
|
||||||
TabModel(label: "Home", icon: "house.fill", view: .home),
|
|
||||||
TabModel(label: "Shelf", icon: "tray.fill", view: .shelf),
|
|
||||||
TabModel(label: "Timer", icon: "timer", view: .timer),
|
|
||||||
TabModel(label: "Stats", icon: "chart.line.uptrend.xyaxis", view: .stats)
|
|
||||||
]
|
|
||||||
|
|
||||||
struct TabSelectionView: View {
|
struct TabSelectionView: View {
|
||||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||||
|
@Default(.enableTimerFeature) var enableTimerFeature
|
||||||
|
@Default(.enableStatsFeature) var enableStatsFeature
|
||||||
@Namespace var animation
|
@Namespace var animation
|
||||||
|
|
||||||
|
var availableTabs: [TabModel] {
|
||||||
|
var tabs = [
|
||||||
|
TabModel(label: "Home", icon: "house.fill", view: .home),
|
||||||
|
TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)
|
||||||
|
]
|
||||||
|
|
||||||
|
if enableTimerFeature {
|
||||||
|
tabs.append(TabModel(label: "Timer", icon: "timer", view: .timer))
|
||||||
|
}
|
||||||
|
|
||||||
|
if enableStatsFeature {
|
||||||
|
tabs.append(TabModel(label: "Stats", icon: "chart.line.uptrend.xyaxis", view: .stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ForEach(tabs) { tab in
|
ForEach(availableTabs) { tab in
|
||||||
TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) {
|
TabButton(label: tab.label, icon: tab.icon, selected: coordinator.currentView == tab.view) {
|
||||||
withAnimation(.smooth) {
|
withAnimation(.smooth) {
|
||||||
coordinator.currentView = tab.view
|
coordinator.currentView = tab.view
|
||||||
|
|
|
||||||
|
|
@ -256,19 +256,37 @@ class TimerManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func playTimerSound() {
|
private func playTimerSound() {
|
||||||
// Play default macOS timer sound
|
var soundURL: URL?
|
||||||
guard let soundURL = Bundle.main.url(forResource: "dynamic", withExtension: "m4a") else {
|
|
||||||
// Fallback to system sound
|
// Check for custom timer sound first
|
||||||
|
let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath")
|
||||||
|
if let customPath = customTimerSoundPath, !customPath.isEmpty {
|
||||||
|
// Use custom sound file
|
||||||
|
soundURL = URL(fileURLWithPath: customPath)
|
||||||
|
|
||||||
|
// Verify the file exists
|
||||||
|
if !FileManager.default.fileExists(atPath: customPath) {
|
||||||
|
soundURL = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default sound if no custom sound or custom sound doesn't exist
|
||||||
|
if soundURL == nil {
|
||||||
|
soundURL = Bundle.main.url(forResource: "dynamic", withExtension: "m4a")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let finalSoundURL = soundURL else {
|
||||||
|
// Final fallback to system sound
|
||||||
NSSound.beep()
|
NSSound.beep()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
soundPlayer = try AVAudioPlayer(contentsOf: soundURL)
|
soundPlayer = try AVAudioPlayer(contentsOf: finalSoundURL)
|
||||||
soundPlayer?.numberOfLoops = -1 // Loop indefinitely
|
soundPlayer?.numberOfLoops = -1 // Loop indefinitely
|
||||||
soundPlayer?.play()
|
soundPlayer?.play()
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback to system sound
|
// Fallback to system sound if there's an error playing the custom sound
|
||||||
NSSound.beep()
|
NSSound.beep()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,9 @@ extension Defaults.Keys {
|
||||||
// MARK: Stats Feature
|
// MARK: Stats Feature
|
||||||
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
|
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
|
||||||
|
|
||||||
|
// MARK: Timer Feature
|
||||||
|
static let enableTimerFeature = Key<Bool>("enableTimerFeature", default: true)
|
||||||
|
|
||||||
// MARK: Clipboard Feature
|
// MARK: Clipboard Feature
|
||||||
static let enableClipboardManager = Key<Bool>("enableClipboardManager", default: true)
|
static let enableClipboardManager = Key<Bool>("enableClipboardManager", default: true)
|
||||||
static let clipboardHistorySize = Key<Int>("clipboardHistorySize", default: 3)
|
static let clipboardHistorySize = Key<Int>("clipboardHistorySize", default: 3)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue