mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update settings search
This commit is contained in:
parent
a08540c589
commit
5790b96cfe
3 changed files with 730 additions and 361 deletions
|
|
@ -15,9 +15,106 @@ import SwiftUI
|
|||
import SwiftUIIntrospect
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
private enum SettingsTab: String, CaseIterable, Identifiable {
|
||||
case general
|
||||
case appearance
|
||||
case lockScreen
|
||||
case media
|
||||
case timer
|
||||
case calendar
|
||||
case hud
|
||||
case battery
|
||||
case stats
|
||||
case clipboard
|
||||
case screenAssistant
|
||||
case colorPicker
|
||||
case downloads
|
||||
case shelf
|
||||
case shortcuts
|
||||
case extensions
|
||||
case about
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .general: return "General"
|
||||
case .appearance: return "Appearance"
|
||||
case .lockScreen: return "Lock Screen"
|
||||
case .media: return "Media"
|
||||
case .timer: return "Timer"
|
||||
case .calendar: return "Calendar"
|
||||
case .hud: return "HUDs"
|
||||
case .battery: return "Battery"
|
||||
case .stats: return "Stats"
|
||||
case .clipboard: return "Clipboard"
|
||||
case .screenAssistant: return "Screen Assistant"
|
||||
case .colorPicker: return "Color Picker"
|
||||
case .downloads: return "Downloads"
|
||||
case .shelf: return "Shelf"
|
||||
case .shortcuts: return "Shortcuts"
|
||||
case .extensions: return "Extensions"
|
||||
case .about: return "About"
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .general: return "gear"
|
||||
case .appearance: return "paintpalette"
|
||||
case .lockScreen: return "lock.laptopcomputer"
|
||||
case .media: return "play.laptopcomputer"
|
||||
case .timer: return "timer"
|
||||
case .calendar: return "calendar"
|
||||
case .hud: return "dial.medium.fill"
|
||||
case .battery: return "battery.100.bolt"
|
||||
case .stats: return "chart.xyaxis.line"
|
||||
case .clipboard: return "clipboard"
|
||||
case .screenAssistant: return "brain.head.profile"
|
||||
case .colorPicker: return "eyedropper"
|
||||
case .downloads: return "square.and.arrow.down"
|
||||
case .shelf: return "books.vertical"
|
||||
case .shortcuts: return "keyboard"
|
||||
case .extensions: return "puzzlepiece.extension"
|
||||
case .about: return "info.circle"
|
||||
}
|
||||
}
|
||||
|
||||
var tint: Color {
|
||||
switch self {
|
||||
case .general: return Color.blue
|
||||
case .appearance: return Color.purple
|
||||
case .lockScreen: return Color.orange
|
||||
case .media: return Color.green
|
||||
case .timer: return Color.red
|
||||
case .calendar: return Color.cyan
|
||||
case .hud: return Color.indigo
|
||||
case .battery: return Color.yellow
|
||||
case .stats: return Color.teal
|
||||
case .clipboard: return Color.mint
|
||||
case .screenAssistant: return Color.pink
|
||||
case .colorPicker: return Color.accentColor
|
||||
case .downloads: return Color.gray
|
||||
case .shelf: return Color.brown
|
||||
case .shortcuts: return Color.orange
|
||||
case .extensions: return Color.blue
|
||||
case .about: return Color.secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SettingsSearchEntry: Identifiable {
|
||||
let tab: SettingsTab
|
||||
let title: String
|
||||
let keywords: [String]
|
||||
|
||||
var id: String { "\(tab.rawValue)-\(title)" }
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@StateObject var extensionManager = DynamicIslandExtensionManager()
|
||||
@State private var selectedTab = "General"
|
||||
@State private var selectedTab: SettingsTab = .general
|
||||
@State private var searchText: String = ""
|
||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||
|
||||
let updaterController: SPUStandardUpdaterController?
|
||||
|
|
@ -28,111 +125,40 @@ struct SettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List(selection: $selectedTab) {
|
||||
NavigationLink(value: "General") {
|
||||
Label("General", systemImage: "gear")
|
||||
}
|
||||
NavigationLink(value: "Appearance") {
|
||||
Label("Appearance", systemImage: "eye")
|
||||
}
|
||||
NavigationLink(value: "Media") {
|
||||
Label("Media", systemImage: "play.laptopcomputer")
|
||||
}
|
||||
NavigationLink(value: "Calendar") {
|
||||
Label("Calendar", systemImage: "calendar")
|
||||
}
|
||||
NavigationLink(value: "HUD") {
|
||||
Label("HUDs", systemImage: "dial.medium.fill")
|
||||
}
|
||||
NavigationLink(value: "Battery") {
|
||||
Label("Battery", systemImage: "battery.100.bolt")
|
||||
}
|
||||
if !enableMinimalisticUI {
|
||||
NavigationLink(value: "Timer") {
|
||||
Label("Timer", systemImage: "timer")
|
||||
}
|
||||
NavigationLink(value: "Stats") {
|
||||
Label("Stats", systemImage: "chart.xyaxis.line")
|
||||
}
|
||||
NavigationLink(value: "Clipboard") {
|
||||
Label("Clipboard", systemImage: "clipboard")
|
||||
}
|
||||
NavigationLink(value: "ScreenAssistant") {
|
||||
Label("Screen Assistant", systemImage: "brain.head.profile")
|
||||
}
|
||||
NavigationLink(value: "ColorPicker") {
|
||||
Label("Color Picker", systemImage: "eyedropper")
|
||||
List(filteredTabs, selection: selectionBinding) { tab in
|
||||
NavigationLink(value: tab) {
|
||||
HStack(spacing: 10) {
|
||||
sidebarIcon(for: tab)
|
||||
Text(tab.title)
|
||||
.foregroundStyle(Color.primary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
if extensionManager.installedExtensions
|
||||
.contains(where: { $0.bundleIdentifier == downloadManagerExtension }) {
|
||||
NavigationLink(value: "Downloads") {
|
||||
Label("Downloads", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
if !enableMinimalisticUI {
|
||||
NavigationLink(value: "Shelf") {
|
||||
Label("Shelf", systemImage: "books.vertical")
|
||||
}
|
||||
}
|
||||
NavigationLink(value: "Shortcuts") {
|
||||
Label("Shortcuts", systemImage: "keyboard")
|
||||
}
|
||||
NavigationLink(value: "Extensions") {
|
||||
Label("Extensions", systemImage: "puzzlepiece.extension")
|
||||
}
|
||||
NavigationLink(value: "About") {
|
||||
Label("About", systemImage: "info.circle")
|
||||
}
|
||||
.tag(tab)
|
||||
.listRowInsets(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 10))
|
||||
}
|
||||
.listStyle(SidebarListStyle())
|
||||
.frame(minWidth: 205)
|
||||
.toolbar(removing: .sidebarToggle)
|
||||
.navigationSplitViewColumnWidth(200)
|
||||
} detail: {
|
||||
Group {
|
||||
switch selectedTab {
|
||||
case "General":
|
||||
GeneralSettings()
|
||||
case "Appearance":
|
||||
Appearance()
|
||||
case "Media":
|
||||
Media()
|
||||
case "Calendar":
|
||||
CalendarSettings()
|
||||
case "HUD":
|
||||
HUD()
|
||||
case "Battery":
|
||||
Charge()
|
||||
case "Timer":
|
||||
TimerSettings()
|
||||
case "Stats":
|
||||
StatsSettings()
|
||||
case "Clipboard":
|
||||
ClipboardSettings()
|
||||
case "ScreenAssistant":
|
||||
ScreenAssistantSettings()
|
||||
case "ColorPicker":
|
||||
ColorPickerSettings()
|
||||
case "Downloads":
|
||||
Downloads()
|
||||
case "Shelf":
|
||||
Shelf()
|
||||
case "Shortcuts":
|
||||
Shortcuts()
|
||||
case "Extensions":
|
||||
Extensions()
|
||||
case "About":
|
||||
if let controller = updaterController {
|
||||
About(updaterController: controller)
|
||||
} else {
|
||||
// Fallback with a default controller
|
||||
About(updaterController: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil))
|
||||
.navigationSplitViewColumnWidth(min: 220, ideal: 240, max: 360)
|
||||
.searchable(text: $searchText, placement: .sidebar, prompt: Text("Search Settings"))
|
||||
.searchSuggestions {
|
||||
ForEach(searchSuggestions) { suggestion in
|
||||
Button {
|
||||
selectedTab = suggestion.tab
|
||||
} label: {
|
||||
Label {
|
||||
Text("\(suggestion.title) — \(suggestion.tab.title)")
|
||||
} icon: {
|
||||
Image(systemName: suggestion.tab.systemImage)
|
||||
}
|
||||
}
|
||||
default:
|
||||
GeneralSettings()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.environment(\.defaultMinListRowHeight, 44)
|
||||
} detail: {
|
||||
detailView(for: resolvedSelection)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
.toolbar(removing: .sidebarToggle)
|
||||
|
|
@ -145,7 +171,311 @@ struct SettingsView: View {
|
|||
.environmentObject(extensionManager)
|
||||
.formStyle(.grouped)
|
||||
.frame(width: 700)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
.onChange(of: searchText) { _, newValue in
|
||||
let matches = tabsMatchingSearch(newValue)
|
||||
guard let firstMatch = matches.first else { return }
|
||||
if !matches.contains(resolvedSelection) {
|
||||
selectedTab = firstMatch
|
||||
}
|
||||
}
|
||||
.background {
|
||||
Group {
|
||||
if #available(macOS 26.0, *) {
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.glassEffect(
|
||||
.clear
|
||||
.tint(Color.white.opacity(0.1))
|
||||
.interactive(),
|
||||
in: .rect(cornerRadius: 18)
|
||||
)
|
||||
} else {
|
||||
ZStack {
|
||||
Color(NSColor.windowBackgroundColor)
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
private var resolvedSelection: SettingsTab {
|
||||
availableTabs.contains(selectedTab) ? selectedTab : (availableTabs.first ?? .general)
|
||||
}
|
||||
|
||||
private var filteredTabs: [SettingsTab] {
|
||||
tabsMatchingSearch(searchText)
|
||||
}
|
||||
|
||||
private var selectionBinding: Binding<SettingsTab> {
|
||||
Binding(
|
||||
get: { resolvedSelection },
|
||||
set: { selectedTab = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sidebarIcon(for tab: SettingsTab) -> some View {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(tab.tint)
|
||||
.frame(width: 26, height: 26)
|
||||
.overlay {
|
||||
Image(systemName: tab.systemImage)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(Color.white)
|
||||
}
|
||||
}
|
||||
|
||||
private var availableTabs: [SettingsTab] {
|
||||
let ordered: [SettingsTab] = [
|
||||
.general,
|
||||
.appearance,
|
||||
.lockScreen,
|
||||
.media,
|
||||
.timer,
|
||||
.calendar,
|
||||
.hud,
|
||||
.battery,
|
||||
.stats,
|
||||
.clipboard,
|
||||
.screenAssistant,
|
||||
.colorPicker,
|
||||
.downloads,
|
||||
.shelf,
|
||||
.shortcuts,
|
||||
.extensions,
|
||||
.about
|
||||
]
|
||||
|
||||
return ordered.filter { isTabVisible($0) }
|
||||
}
|
||||
|
||||
private func tabsMatchingSearch(_ query: String) -> [SettingsTab] {
|
||||
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return availableTabs }
|
||||
|
||||
let entryMatches = searchEntries(matching: trimmed)
|
||||
let matchingTabs = Set(entryMatches.map(\.tab))
|
||||
|
||||
return availableTabs.filter { tab in
|
||||
tab.title.localizedCaseInsensitiveContains(trimmed) || matchingTabs.contains(tab)
|
||||
}
|
||||
}
|
||||
|
||||
private var searchSuggestions: [SettingsSearchEntry] {
|
||||
Array(searchEntries(matching: searchText).prefix(8))
|
||||
}
|
||||
|
||||
private func searchEntries(matching query: String) -> [SettingsSearchEntry] {
|
||||
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return [] }
|
||||
|
||||
return settingsSearchIndex
|
||||
.filter { availableTabs.contains($0.tab) }
|
||||
.filter { entry in
|
||||
entry.title.localizedCaseInsensitiveContains(trimmed) ||
|
||||
entry.keywords.contains { $0.localizedCaseInsensitiveContains(trimmed) }
|
||||
}
|
||||
}
|
||||
|
||||
private var settingsSearchIndex: [SettingsSearchEntry] {
|
||||
[
|
||||
// General
|
||||
SettingsSearchEntry(tab: .general, title: "Enable Minimalistic UI", keywords: ["minimalistic", "ui mode", "general"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Menubar icon", keywords: ["menu bar", "status bar", "icon"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Launch at login", keywords: ["autostart", "startup"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Show on all displays", keywords: ["multi-display", "external monitor"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Show on a specific display", keywords: ["preferred screen", "display picker"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Automatically switch displays", keywords: ["auto switch", "displays"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Hide Dynamic Island during screenshots & recordings", keywords: ["privacy", "screenshot", "recording"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Enable Screen Recording Detection", keywords: ["screen recording", "indicator"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Show Recording Indicator", keywords: ["recording indicator", "red dot"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Enable Focus Detection", keywords: ["focus", "do not disturb", "dnd"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Show Focus Indicator", keywords: ["focus icon", "moon"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Show Focus Label", keywords: ["focus label", "text"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Enable Camera Detection", keywords: ["camera", "privacy indicator"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Enable Microphone Detection", keywords: ["microphone", "privacy"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Enable gestures", keywords: ["gestures", "trackpad"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Close gesture", keywords: ["pinch", "swipe"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Extend hover area", keywords: ["hover", "cursor"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Enable haptics", keywords: ["haptic", "feedback"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Open notch on hover", keywords: ["hover to open", "auto open"]),
|
||||
SettingsSearchEntry(tab: .general, title: "Non-notch display height", keywords: ["display height", "menu bar size"]),
|
||||
|
||||
// Battery (Charge)
|
||||
SettingsSearchEntry(tab: .battery, title: "Show battery indicator", keywords: ["battery hud", "charge"]),
|
||||
SettingsSearchEntry(tab: .battery, title: "Show battery percentage", keywords: ["battery percent"]),
|
||||
SettingsSearchEntry(tab: .battery, title: "Show power status notifications", keywords: ["notifications", "power"]),
|
||||
SettingsSearchEntry(tab: .battery, title: "Show power status icons", keywords: ["power icons", "charging icon"]),
|
||||
|
||||
// Downloads
|
||||
SettingsSearchEntry(tab: .downloads, title: "Show download progress", keywords: ["downloads", "progress"]),
|
||||
SettingsSearchEntry(tab: .downloads, title: "Enable Safari Downloads", keywords: ["safari", "browser"]),
|
||||
SettingsSearchEntry(tab: .downloads, title: "Download indicator style", keywords: ["indicator", "style"]),
|
||||
SettingsSearchEntry(tab: .downloads, title: "Download icon style", keywords: ["icon", "style"]),
|
||||
|
||||
// HUDs
|
||||
SettingsSearchEntry(tab: .hud, title: "Show Bluetooth device connections", keywords: ["bluetooth", "hud"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Use circular battery indicator", keywords: ["battery", "circular"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Show battery percentage text in HUD", keywords: ["battery text"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Scroll device name in HUD", keywords: ["marquee", "device name"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Color-coded battery display", keywords: ["color", "battery"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Color-coded volume display", keywords: ["volume", "color"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Smooth color transitions", keywords: ["gradient", "smooth"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Show percentages beside progress bars", keywords: ["percentages", "progress"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "HUD style", keywords: ["inline", "compact"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Progressbar style", keywords: ["progress", "style"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Enable glowing effect", keywords: ["glow", "indicator"]),
|
||||
SettingsSearchEntry(tab: .hud, title: "Use accent color", keywords: ["accent", "color"]),
|
||||
|
||||
// Media
|
||||
SettingsSearchEntry(tab: .media, title: "Music Source", keywords: ["media source", "controller"]),
|
||||
SettingsSearchEntry(tab: .media, title: "Skip buttons", keywords: ["skip", "controls", "±10"]),
|
||||
SettingsSearchEntry(tab: .media, title: "Sneak Peek Style", keywords: ["sneak peek", "preview"]),
|
||||
SettingsSearchEntry(tab: .media, title: "Enable lyrics", keywords: ["lyrics", "song text"]),
|
||||
|
||||
// Calendar
|
||||
SettingsSearchEntry(tab: .calendar, title: "Show calendar", keywords: ["calendar", "events"]),
|
||||
SettingsSearchEntry(tab: .calendar, title: "Enable reminder live activity", keywords: ["reminder", "live activity"]),
|
||||
SettingsSearchEntry(tab: .calendar, title: "Countdown style", keywords: ["reminder countdown"]),
|
||||
SettingsSearchEntry(tab: .calendar, title: "Show lock screen reminder", keywords: ["lock screen", "reminder widget"]),
|
||||
SettingsSearchEntry(tab: .calendar, title: "Chip color", keywords: ["reminder chip", "color"]),
|
||||
|
||||
// Shelf
|
||||
SettingsSearchEntry(tab: .shelf, title: "Enable shelf", keywords: ["shelf", "dock"]),
|
||||
SettingsSearchEntry(tab: .shelf, title: "Open shelf tab by default if items added", keywords: ["auto open", "shelf tab"]),
|
||||
|
||||
// Appearance
|
||||
SettingsSearchEntry(tab: .appearance, title: "Settings icon in notch", keywords: ["settings button", "toolbar"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Enable window shadow", keywords: ["shadow", "appearance"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Corner radius scaling", keywords: ["corner radius", "shape"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Use simpler close animation", keywords: ["close animation", "notch"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Enable colored spectrograms", keywords: ["spectrogram", "audio"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Enable blur effect behind album art", keywords: ["blur", "album art"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Slider color", keywords: ["slider", "accent"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Enable Dynamic mirror", keywords: ["mirror", "reflection"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Mirror shape", keywords: ["mirror shape", "circle", "rectangle"]),
|
||||
SettingsSearchEntry(tab: .appearance, title: "Show cool face animation while inactivity", keywords: ["face animation", "idle"]),
|
||||
|
||||
// Lock Screen
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Enable lock screen live activity", keywords: ["lock screen", "live activity"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Play lock/unlock sounds", keywords: ["chime", "sound"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Material", keywords: ["glass", "frosted", "liquid"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show lock screen media panel", keywords: ["media panel", "lock screen media"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show media app icon", keywords: ["app icon", "media"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show panel border", keywords: ["panel border"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Enable media panel blur", keywords: ["blur", "media panel"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show lock screen timer", keywords: ["timer widget", "lock screen timer"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Enable timer blur", keywords: ["timer blur"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show lock screen weather", keywords: ["weather widget"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Layout", keywords: ["inline", "circular", "weather layout"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Weather data provider", keywords: ["wttr", "open meteo"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Temperature unit", keywords: ["celsius", "fahrenheit"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show location label", keywords: ["location", "weather"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show charging status", keywords: ["charging", "weather"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show charging percentage", keywords: ["charging percentage"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show battery indicator", keywords: ["battery gauge", "weather"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Use MacBook icon when on battery", keywords: ["laptop icon", "battery"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show Bluetooth battery", keywords: ["bluetooth", "gauge"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Show AQI widget", keywords: ["air quality", "aqi"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Air quality scale", keywords: ["aqi", "scale"]),
|
||||
SettingsSearchEntry(tab: .lockScreen, title: "Use colored gauges", keywords: ["gauge tint", "monochrome"]),
|
||||
|
||||
// Shortcuts
|
||||
SettingsSearchEntry(tab: .shortcuts, title: "Enable global keyboard shortcuts", keywords: ["keyboard", "shortcut"]),
|
||||
|
||||
// Timer
|
||||
SettingsSearchEntry(tab: .timer, title: "Enable timer feature", keywords: ["timer", "enable"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Mirror macOS Clock timers", keywords: ["system timer", "clock app"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Show lock screen timer widget", keywords: ["lock screen", "timer widget"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Enable timer blur", keywords: ["timer blur", "lock screen"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Timer tint", keywords: ["timer colour", "preset"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Solid colour", keywords: ["timer colour", "custom"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Progress style", keywords: ["progress", "bar", "ring"]),
|
||||
SettingsSearchEntry(tab: .timer, title: "Accent colour", keywords: ["accent", "timer"]),
|
||||
|
||||
// Stats
|
||||
SettingsSearchEntry(tab: .stats, title: "Enable system stats monitoring", keywords: ["stats", "monitoring"]),
|
||||
SettingsSearchEntry(tab: .stats, title: "Stop monitoring after closing the notch", keywords: ["stats", "auto stop"]),
|
||||
SettingsSearchEntry(tab: .stats, title: "CPU Usage", keywords: ["cpu", "graph"]),
|
||||
SettingsSearchEntry(tab: .stats, title: "Memory Usage", keywords: ["memory", "ram"]),
|
||||
SettingsSearchEntry(tab: .stats, title: "GPU Usage", keywords: ["gpu", "graphics"]),
|
||||
SettingsSearchEntry(tab: .stats, title: "Network Activity", keywords: ["network", "graph"]),
|
||||
SettingsSearchEntry(tab: .stats, title: "Disk I/O", keywords: ["disk", "io"]),
|
||||
|
||||
// Clipboard
|
||||
SettingsSearchEntry(tab: .clipboard, title: "Enable Clipboard Manager", keywords: ["clipboard", "manager"]),
|
||||
SettingsSearchEntry(tab: .clipboard, title: "Show Clipboard Icon", keywords: ["icon", "clipboard"]),
|
||||
SettingsSearchEntry(tab: .clipboard, title: "Display Mode", keywords: ["list", "grid", "clipboard"]),
|
||||
SettingsSearchEntry(tab: .clipboard, title: "History Size", keywords: ["history", "clipboard"]),
|
||||
|
||||
// Screen Assistant
|
||||
SettingsSearchEntry(tab: .screenAssistant, title: "Enable Screen Assistant", keywords: ["screen assistant", "ai"]),
|
||||
SettingsSearchEntry(tab: .screenAssistant, title: "Display Mode", keywords: ["screen assistant", "mode"]),
|
||||
|
||||
// Color Picker
|
||||
SettingsSearchEntry(tab: .colorPicker, title: "Enable Color Picker", keywords: ["color picker", "eyedropper"]),
|
||||
SettingsSearchEntry(tab: .colorPicker, title: "Show Color Picker Icon", keywords: ["color icon", "toolbar"]),
|
||||
SettingsSearchEntry(tab: .colorPicker, title: "Display Mode", keywords: ["color", "list"]),
|
||||
SettingsSearchEntry(tab: .colorPicker, title: "History Size", keywords: ["color history"]),
|
||||
SettingsSearchEntry(tab: .colorPicker, title: "Show All Color Formats", keywords: ["hex", "hsl", "color formats"])
|
||||
]
|
||||
}
|
||||
|
||||
private func isTabVisible(_ tab: SettingsTab) -> Bool {
|
||||
switch tab {
|
||||
case .timer, .stats, .clipboard, .screenAssistant, .colorPicker, .shelf:
|
||||
return !enableMinimalisticUI
|
||||
case .downloads:
|
||||
return extensionManager.installedExtensions.contains { $0.bundleIdentifier == downloadManagerExtension }
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func detailView(for tab: SettingsTab) -> some View {
|
||||
switch tab {
|
||||
case .general:
|
||||
GeneralSettings()
|
||||
case .appearance:
|
||||
Appearance()
|
||||
case .lockScreen:
|
||||
LockScreenSettings()
|
||||
case .media:
|
||||
Media()
|
||||
case .timer:
|
||||
TimerSettings()
|
||||
case .calendar:
|
||||
CalendarSettings()
|
||||
case .hud:
|
||||
HUD()
|
||||
case .battery:
|
||||
Charge()
|
||||
case .stats:
|
||||
StatsSettings()
|
||||
case .clipboard:
|
||||
ClipboardSettings()
|
||||
case .screenAssistant:
|
||||
ScreenAssistantSettings()
|
||||
case .colorPicker:
|
||||
ColorPickerSettings()
|
||||
case .downloads:
|
||||
Downloads()
|
||||
case .shelf:
|
||||
Shelf()
|
||||
case .shortcuts:
|
||||
Shortcuts()
|
||||
case .extensions:
|
||||
Extensions()
|
||||
case .about:
|
||||
if let controller = updaterController {
|
||||
About(updaterController: controller)
|
||||
} else {
|
||||
About(updaterController: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,24 +505,6 @@ struct GeneralSettings: View {
|
|||
@Default(.showDoNotDisturbIndicator) var showDoNotDisturbIndicator
|
||||
@Default(.showDoNotDisturbLabel) var showDoNotDisturbLabel
|
||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||
@Default(.lockScreenShowAppIcon) var lockScreenShowAppIcon
|
||||
@Default(.lockScreenPanelShowsBorder) var lockScreenPanelShowsBorder
|
||||
@Default(.lockScreenPanelUsesBlur) var lockScreenPanelUsesBlur
|
||||
@Default(.enableLockScreenTimerWidget) var enableLockScreenTimerWidget
|
||||
@Default(.enableLockScreenWeatherWidget) var enableLockScreenWeatherWidget
|
||||
@Default(.lockScreenWeatherShowsLocation) var lockScreenWeatherShowsLocation
|
||||
@Default(.lockScreenWeatherShowsCharging) var lockScreenWeatherShowsCharging
|
||||
@Default(.lockScreenWeatherShowsChargingPercentage) var lockScreenWeatherShowsChargingPercentage
|
||||
@Default(.lockScreenWeatherShowsBluetooth) var lockScreenWeatherShowsBluetooth
|
||||
@Default(.lockScreenWeatherShowsBatteryGauge) var lockScreenWeatherShowsBatteryGauge
|
||||
@Default(.lockScreenWeatherWidgetStyle) var lockScreenWeatherWidgetStyle
|
||||
@Default(.lockScreenWeatherTemperatureUnit) var lockScreenWeatherTemperatureUnit
|
||||
@Default(.lockScreenWeatherShowsAQI) var lockScreenWeatherShowsAQI
|
||||
@Default(.lockScreenWeatherAQIScale) var lockScreenWeatherAQIScale
|
||||
@Default(.lockScreenWeatherUsesGaugeTint) var lockScreenWeatherUsesGaugeTint
|
||||
@Default(.lockScreenWeatherProviderSource) var lockScreenWeatherProviderSource
|
||||
@Default(.lockScreenWeatherBatteryUsesLaptopSymbol) var lockScreenWeatherBatteryUsesLaptopSymbol
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
|
@ -355,97 +667,6 @@ struct GeneralSettings: View {
|
|||
Text("Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Enable Lock Screen Live Activity", key: .enableLockScreenLiveActivity)
|
||||
Defaults.Toggle("Play lock/unlock sounds", key: .enableLockSounds)
|
||||
if #available(macOS 26.0, *) {
|
||||
Picker("Lock screen material", selection: $lockScreenGlassStyle) {
|
||||
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||
Text(style.rawValue).tag(style)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Picker("Lock screen material", selection: $lockScreenGlassStyle) {
|
||||
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||
Text(style.rawValue).tag(style)
|
||||
}
|
||||
}
|
||||
.disabled(true)
|
||||
Text("Liquid Glass requires macOS 26 or later.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
||||
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||
Defaults.Toggle("Enable blur", key: .lockScreenPanelUsesBlur)
|
||||
Defaults.Toggle("Show lock screen media panel", key: .enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Show lock screen weather", key: .enableLockScreenWeatherWidget)
|
||||
Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget)
|
||||
Defaults.Toggle("Enable timer blur", key: .lockScreenTimerWidgetUsesBlur)
|
||||
.disabled(!enableLockScreenTimerWidget)
|
||||
if enableLockScreenWeatherWidget {
|
||||
Picker("Widget layout", selection: $lockScreenWeatherWidgetStyle) {
|
||||
ForEach(LockScreenWeatherWidgetStyle.allCases) { style in
|
||||
Text(style.rawValue).tag(style)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Picker("Weather data provider", selection: $lockScreenWeatherProviderSource) {
|
||||
ForEach(LockScreenWeatherProviderSource.allCases) { source in
|
||||
Text(source.displayName).tag(source)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Picker("Temperature unit", selection: $lockScreenWeatherTemperatureUnit) {
|
||||
ForEach(LockScreenWeatherTemperatureUnit.allCases) { unit in
|
||||
Text(unit.rawValue).tag(unit)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation)
|
||||
.disabled(lockScreenWeatherWidgetStyle == .circular)
|
||||
Defaults.Toggle("Show charging status", key: .lockScreenWeatherShowsCharging)
|
||||
if lockScreenWeatherShowsCharging {
|
||||
Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage)
|
||||
}
|
||||
Defaults.Toggle("Show battery indicator", key: .lockScreenWeatherShowsBatteryGauge)
|
||||
if lockScreenWeatherShowsBatteryGauge {
|
||||
Defaults.Toggle("Use MacBook icon when on battery", key: .lockScreenWeatherBatteryUsesLaptopSymbol)
|
||||
.disabled(lockScreenWeatherWidgetStyle != .circular)
|
||||
if lockScreenWeatherWidgetStyle != .circular {
|
||||
Text("MacBook icon is available in the circular layout.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Defaults.Toggle("Show Bluetooth battery", key: .lockScreenWeatherShowsBluetooth)
|
||||
Defaults.Toggle("Show AQI widget", key: .lockScreenWeatherShowsAQI)
|
||||
.disabled(!lockScreenWeatherProviderSource.supportsAirQuality)
|
||||
if lockScreenWeatherShowsAQI && lockScreenWeatherProviderSource.supportsAirQuality {
|
||||
Picker("Air quality scale", selection: $lockScreenWeatherAQIScale) {
|
||||
ForEach(LockScreenWeatherAirQualityScale.allCases) { scale in
|
||||
Text(scale.displayName).tag(scale)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
if !lockScreenWeatherProviderSource.supportsAirQuality {
|
||||
Text("Air quality requires the Open Meteo provider.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Defaults.Toggle("Use colored gauges", key: .lockScreenWeatherUsesGaugeTint)
|
||||
}
|
||||
|
||||
Button("Copy Latest Crash Report") {
|
||||
copyLatestCrashReport()
|
||||
}
|
||||
} header: {
|
||||
Text("Lock Screen")
|
||||
} footer: {
|
||||
Text("Shows a lock icon in the Dynamic Island when the screen is locked. Use the toggles above to control the lock screen media panel and weather capsule.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker(selection: $notchHeightMode, label:
|
||||
Text("Notch display height")) {
|
||||
|
|
@ -524,43 +745,6 @@ struct GeneralSettings: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func copyLatestCrashReport() {
|
||||
let crashReportsPath = NSString(string: "~/Library/Logs/DiagnosticReports").expandingTildeInPath
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(atPath: crashReportsPath)
|
||||
let crashFiles = files.filter { $0.contains("DynamicIsland") && $0.hasSuffix(".crash") }
|
||||
|
||||
guard let latestCrash = crashFiles.sorted(by: >).first else {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "No Crash Reports Found"
|
||||
alert.informativeText = "No crash reports found for DynamicIsland"
|
||||
alert.alertStyle = .informational
|
||||
alert.runModal()
|
||||
return
|
||||
}
|
||||
|
||||
let crashPath = (crashReportsPath as NSString).appendingPathComponent(latestCrash)
|
||||
let crashContent = try String(contentsOfFile: crashPath, encoding: .utf8)
|
||||
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(crashContent, forType: .string)
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Crash Report Copied"
|
||||
alert.informativeText = "Crash report '\(latestCrash)' has been copied to clipboard"
|
||||
alert.alertStyle = .informational
|
||||
alert.runModal()
|
||||
} catch {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Error"
|
||||
alert.informativeText = "Failed to read crash reports: \(error.localizedDescription)"
|
||||
alert.alertStyle = .warning
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func gestureControls() -> some View {
|
||||
Section {
|
||||
|
|
@ -864,6 +1048,7 @@ struct Media: View {
|
|||
@Default(.musicAuxRightControl) private var musicAuxRightControl
|
||||
@Default(.musicSkipBehavior) private var musicSkipBehavior
|
||||
@Default(.musicControlWindowEnabled) private var musicControlWindowEnabled
|
||||
@Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
|
||||
@State private var previousLeftAuxControl: MusicAuxiliaryControl = Defaults[.musicAuxLeftControl]
|
||||
@State private var previousRightAuxControl: MusicAuxiliaryControl = Defaults[.musicAuxRightControl]
|
||||
|
||||
|
|
@ -976,6 +1161,20 @@ struct Media: View {
|
|||
Text("Media playback live activity")
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Show lock screen media panel", key: .enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
||||
.disabled(!enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||
.disabled(!enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur)
|
||||
.disabled(!enableLockScreenMediaWidget)
|
||||
} header: {
|
||||
Text("Lock Screen Integration")
|
||||
} footer: {
|
||||
Text("These controls mirror the Lock Screen tab so you can tune the media overlay while focusing on playback settings.")
|
||||
}
|
||||
|
||||
Picker(selection: $hideNotchOption, label:
|
||||
HStack {
|
||||
Text("Hide DynamicIsland Options")
|
||||
|
|
@ -1370,7 +1569,7 @@ struct About: View {
|
|||
}
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
Text("Made ❤️ by Ebullioscopic")
|
||||
Text("Made with ❤️ by Ebullioscopic")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 7)
|
||||
|
|
@ -1552,8 +1751,6 @@ struct Appearance: View {
|
|||
Text("General")
|
||||
}
|
||||
|
||||
LockScreenPositioningControls()
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram)
|
||||
Defaults
|
||||
|
|
@ -1811,6 +2008,162 @@ struct Appearance: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct LockScreenSettings: View {
|
||||
@Default(.lockScreenGlassStyle) private var lockScreenGlassStyle
|
||||
@Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
|
||||
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
|
||||
@Default(.enableLockScreenWeatherWidget) private var enableLockScreenWeatherWidget
|
||||
@Default(.lockScreenWeatherWidgetStyle) private var lockScreenWeatherWidgetStyle
|
||||
@Default(.lockScreenWeatherProviderSource) private var lockScreenWeatherProviderSource
|
||||
@Default(.lockScreenWeatherTemperatureUnit) private var lockScreenWeatherTemperatureUnit
|
||||
@Default(.lockScreenWeatherShowsCharging) private var lockScreenWeatherShowsCharging
|
||||
@Default(.lockScreenWeatherShowsBatteryGauge) private var lockScreenWeatherShowsBatteryGauge
|
||||
@Default(.lockScreenWeatherShowsAQI) private var lockScreenWeatherShowsAQI
|
||||
@Default(.lockScreenWeatherAQIScale) private var lockScreenWeatherAQIScale
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Defaults.Toggle("Enable lock screen live activity", key: .enableLockScreenLiveActivity)
|
||||
Defaults.Toggle("Play lock/unlock sounds", key: .enableLockSounds)
|
||||
} header: {
|
||||
Text("Live Activity & Feedback")
|
||||
} footer: {
|
||||
Text("Controls whether Dynamic Island mirrors lock/unlock events with its own live activity and audible chimes.")
|
||||
}
|
||||
|
||||
Section {
|
||||
if #available(macOS 26.0, *) {
|
||||
Picker("Material", selection: $lockScreenGlassStyle) {
|
||||
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||
Text(style.rawValue).tag(style)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Picker("Material", selection: $lockScreenGlassStyle) {
|
||||
ForEach(LockScreenGlassStyle.allCases) { style in
|
||||
Text(style.rawValue).tag(style)
|
||||
}
|
||||
}
|
||||
.disabled(true)
|
||||
Text("Liquid Glass requires macOS 26 or later.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Defaults.Toggle("Show lock screen media panel", key: .enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
||||
.disabled(!enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||
.disabled(!enableLockScreenMediaWidget)
|
||||
Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur)
|
||||
.disabled(!enableLockScreenMediaWidget)
|
||||
} header: {
|
||||
Text("Media Panel")
|
||||
} footer: {
|
||||
Text("Enable and style the media controls that appear above the system clock when the screen is locked.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget)
|
||||
Defaults.Toggle("Enable timer blur", key: .lockScreenTimerWidgetUsesBlur)
|
||||
.disabled(!enableLockScreenTimerWidget)
|
||||
} header: {
|
||||
Text("Timer Widget")
|
||||
} footer: {
|
||||
Text("Controls the optional timer widget that floats above the media panel. Blur adds a frosted finish behind the compact view.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Show lock screen weather", key: .enableLockScreenWeatherWidget)
|
||||
|
||||
if enableLockScreenWeatherWidget {
|
||||
Picker("Layout", selection: $lockScreenWeatherWidgetStyle) {
|
||||
ForEach(LockScreenWeatherWidgetStyle.allCases) { style in
|
||||
Text(style.rawValue).tag(style)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Picker("Weather data provider", selection: $lockScreenWeatherProviderSource) {
|
||||
ForEach(LockScreenWeatherProviderSource.allCases) { source in
|
||||
Text(source.displayName).tag(source)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Picker("Temperature unit", selection: $lockScreenWeatherTemperatureUnit) {
|
||||
ForEach(LockScreenWeatherTemperatureUnit.allCases) { unit in
|
||||
Text(unit.rawValue).tag(unit)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Defaults.Toggle("Show location label", key: .lockScreenWeatherShowsLocation)
|
||||
.disabled(lockScreenWeatherWidgetStyle == .circular)
|
||||
|
||||
Defaults.Toggle("Show charging status", key: .lockScreenWeatherShowsCharging)
|
||||
|
||||
if lockScreenWeatherShowsCharging {
|
||||
Defaults.Toggle("Show charging percentage", key: .lockScreenWeatherShowsChargingPercentage)
|
||||
}
|
||||
|
||||
Defaults.Toggle("Show battery indicator", key: .lockScreenWeatherShowsBatteryGauge)
|
||||
|
||||
if lockScreenWeatherShowsBatteryGauge {
|
||||
Defaults.Toggle("Use MacBook icon when on battery", key: .lockScreenWeatherBatteryUsesLaptopSymbol)
|
||||
.disabled(lockScreenWeatherWidgetStyle != .circular)
|
||||
|
||||
if lockScreenWeatherWidgetStyle != .circular {
|
||||
Text("The MacBook icon is available only in the circular layout.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Defaults.Toggle("Show Bluetooth battery", key: .lockScreenWeatherShowsBluetooth)
|
||||
|
||||
Defaults.Toggle("Show AQI widget", key: .lockScreenWeatherShowsAQI)
|
||||
.disabled(!lockScreenWeatherProviderSource.supportsAirQuality)
|
||||
|
||||
if lockScreenWeatherShowsAQI && lockScreenWeatherProviderSource.supportsAirQuality {
|
||||
Picker("Air quality scale", selection: $lockScreenWeatherAQIScale) {
|
||||
ForEach(LockScreenWeatherAirQualityScale.allCases) { scale in
|
||||
Text(scale.displayName).tag(scale)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
if !lockScreenWeatherProviderSource.supportsAirQuality {
|
||||
Text("Air quality requires the Open Meteo provider.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Defaults.Toggle("Use colored gauges", key: .lockScreenWeatherUsesGaugeTint)
|
||||
}
|
||||
} header: {
|
||||
Text("Weather Widget")
|
||||
} footer: {
|
||||
Text("Enable the weather capsule and configure its layout, provider, units, and optional battery/AQI indicators.")
|
||||
}
|
||||
|
||||
LockScreenPositioningControls()
|
||||
|
||||
Section {
|
||||
Button("Copy Latest Crash Report") {
|
||||
copyLatestCrashReport()
|
||||
}
|
||||
} header: {
|
||||
Text("Diagnostics")
|
||||
} footer: {
|
||||
Text("Collect the latest crash report to share with the developer when reporting lock screen or overlay issues.")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Lock Screen")
|
||||
}
|
||||
}
|
||||
|
||||
private struct LockScreenPositioningControls: View {
|
||||
@Default(.lockScreenWeatherVerticalOffset) private var weatherOffset
|
||||
@Default(.lockScreenMusicVerticalOffset) private var musicOffset
|
||||
|
|
@ -2163,6 +2516,43 @@ private struct LockScreenPositioningPreview: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func copyLatestCrashReport() {
|
||||
let crashReportsPath = NSString(string: "~/Library/Logs/DiagnosticReports").expandingTildeInPath
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(atPath: crashReportsPath)
|
||||
let crashFiles = files.filter { $0.contains("DynamicIsland") && $0.hasSuffix(".crash") }
|
||||
|
||||
guard let latestCrash = crashFiles.sorted(by: >).first else {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "No Crash Reports Found"
|
||||
alert.informativeText = "No crash reports found for DynamicIsland"
|
||||
alert.alertStyle = .informational
|
||||
alert.runModal()
|
||||
return
|
||||
}
|
||||
|
||||
let crashPath = (crashReportsPath as NSString).appendingPathComponent(latestCrash)
|
||||
let crashContent = try String(contentsOfFile: crashPath, encoding: .utf8)
|
||||
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(crashContent, forType: .string)
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Crash Report Copied"
|
||||
alert.informativeText = "Crash report '\(latestCrash)' has been copied to clipboard"
|
||||
alert.alertStyle = .informational
|
||||
alert.runModal()
|
||||
} catch {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Error"
|
||||
alert.informativeText = "Failed to read crash reports: \(error.localizedDescription)"
|
||||
alert.alertStyle = .warning
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
struct Shortcuts: View {
|
||||
@Default(.enableTimerFeature) var enableTimerFeature
|
||||
@Default(.enableClipboardManager) var enableClipboardManager
|
||||
|
|
@ -2376,6 +2766,7 @@ struct TimerSettings: View {
|
|||
@Default(.timerProgressStyle) private var progressStyle
|
||||
@Default(.timerControlWindowEnabled) private var controlWindowEnabled
|
||||
@Default(.mirrorSystemTimer) private var mirrorSystemTimer
|
||||
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
|
||||
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600
|
||||
@State private var customHours: Int = 0
|
||||
@State private var customMinutes: Int = 10
|
||||
|
|
@ -2401,6 +2792,16 @@ struct TimerSettings: View {
|
|||
|
||||
if enableTimerFeature {
|
||||
Group {
|
||||
Section {
|
||||
Defaults.Toggle("Show lock screen timer widget", key: .enableLockScreenTimerWidget)
|
||||
Defaults.Toggle("Enable timer blur", key: .lockScreenTimerWidgetUsesBlur)
|
||||
.disabled(!enableLockScreenTimerWidget)
|
||||
} header: {
|
||||
Text("Lock Screen Integration")
|
||||
} footer: {
|
||||
Text("Mirrors the toggle found under Lock Screen settings so timer-specific workflows can enable or disable the widget without switching tabs.")
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Default Custom Timer")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue