mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
WIP: Settings
This commit is contained in:
parent
105b0466a4
commit
aeed23f950
9 changed files with 749 additions and 3 deletions
|
|
@ -40,6 +40,8 @@ class CalendarManager: ObservableObject {
|
|||
@Published var reminderAuthorizationStatus: EKAuthorizationStatus = .notDetermined
|
||||
@Published var lockScreenEvents: [EventModel] = []
|
||||
|
||||
private var lockScreenPreviewEvents: [EventModel]?
|
||||
|
||||
private var selectedCalendars: [CalendarModel] = []
|
||||
private let calendarService = CalendarService()
|
||||
private var lastEventsFetchDate: Date?
|
||||
|
|
@ -253,6 +255,16 @@ class CalendarManager: ObservableObject {
|
|||
}
|
||||
|
||||
func updateLockScreenEvents(force: Bool = false) async {
|
||||
if let previewEvents = lockScreenPreviewEvents {
|
||||
if lockScreenEvents != previewEvents {
|
||||
withAnimation(.smooth(duration: 0.25)) {
|
||||
lockScreenEvents = previewEvents
|
||||
}
|
||||
}
|
||||
lastLockScreenEventsFetchDate = Date()
|
||||
return
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
|
||||
if !force,
|
||||
|
|
@ -320,6 +332,18 @@ class CalendarManager: ObservableObject {
|
|||
lastLockScreenEventsFetchDate = Date()
|
||||
}
|
||||
|
||||
func setLockScreenPreviewEvents(_ events: [EventModel]?) {
|
||||
lockScreenPreviewEvents = events
|
||||
guard let events else {
|
||||
Task { await updateLockScreenEvents(force: true) }
|
||||
return
|
||||
}
|
||||
withAnimation(.smooth(duration: 0.25)) {
|
||||
lockScreenEvents = events
|
||||
}
|
||||
lastLockScreenEventsFetchDate = Date()
|
||||
}
|
||||
|
||||
private func startLockScreenRefreshLoop() {
|
||||
lockScreenRefreshTask?.cancel()
|
||||
lockScreenRefreshTask = Task { [weak self] in
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ final class LockScreenTimerWidgetManager {
|
|||
private let timerManager = TimerManager.shared
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var isLocked: Bool = false
|
||||
private var isPreviewing: Bool = false
|
||||
|
||||
private init() {
|
||||
observeTimerState()
|
||||
|
|
@ -55,6 +56,12 @@ final class LockScreenTimerWidgetManager {
|
|||
}
|
||||
}
|
||||
|
||||
func setPreviewMode(_ enabled: Bool) {
|
||||
guard isPreviewing != enabled else { return }
|
||||
isPreviewing = enabled
|
||||
updateVisibility()
|
||||
}
|
||||
|
||||
func refreshPositionForOffsets(animated: Bool) {
|
||||
LockScreenTimerWidgetPanelManager.shared.refreshPosition(animated: animated)
|
||||
}
|
||||
|
|
@ -112,6 +119,9 @@ final class LockScreenTimerWidgetManager {
|
|||
|
||||
private func shouldDisplayWidget() -> Bool {
|
||||
guard Defaults[.enableLockScreenTimerWidget] else { return false }
|
||||
if isPreviewing {
|
||||
return timerManager.isTimerActive
|
||||
}
|
||||
guard isLocked else { return false }
|
||||
return timerManager.hasManualTimerRunning
|
||||
}
|
||||
|
|
|
|||
577
DynamicIsland/managers/LockScreenWidgetPreviewManager.swift
Normal file
577
DynamicIsland/managers/LockScreenWidgetPreviewManager.swift
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
/*
|
||||
* Atoll (DynamicIsland)
|
||||
* Copyright (C) 2024-2026 Atoll Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class LockScreenWidgetPreviewManager: ObservableObject {
|
||||
static let shared = LockScreenWidgetPreviewManager()
|
||||
|
||||
@Published private(set) var isPreviewVisible = false
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var startedDemoTimer = false
|
||||
private var previewWindow: NSWindow?
|
||||
private var previewWindowDelegate: PreviewWindowDelegate?
|
||||
private var previewHostingView: NSHostingView<LockScreenPreviewScene>?
|
||||
private var cachedFocusState: FocusPreviewState?
|
||||
|
||||
private init() {
|
||||
observeDefaults()
|
||||
}
|
||||
|
||||
func togglePreview() {
|
||||
if isPreviewVisible {
|
||||
hidePreview()
|
||||
} else {
|
||||
showPreview()
|
||||
}
|
||||
}
|
||||
|
||||
func showPreview() {
|
||||
guard !isPreviewVisible else { return }
|
||||
isPreviewVisible = true
|
||||
ensurePreviewWindow()
|
||||
applyPreviewState()
|
||||
renderPreview()
|
||||
}
|
||||
|
||||
func hidePreview() {
|
||||
guard isPreviewVisible else { return }
|
||||
isPreviewVisible = false
|
||||
hideAllWidgets()
|
||||
restorePreviewState()
|
||||
closePreviewWindow()
|
||||
}
|
||||
|
||||
private func renderPreview() {
|
||||
guard isPreviewVisible else { return }
|
||||
|
||||
ensurePreviewWindow()
|
||||
|
||||
if Defaults[.enableLockScreenTimerWidget] {
|
||||
ensureDemoTimerIfNeeded()
|
||||
} else {
|
||||
stopDemoTimerIfNeeded()
|
||||
}
|
||||
|
||||
updatePreviewContent()
|
||||
}
|
||||
|
||||
private func hideAllWidgets() {
|
||||
stopDemoTimerIfNeeded()
|
||||
}
|
||||
|
||||
private func ensureDemoTimerIfNeeded() {
|
||||
let timerManager = TimerManager.shared
|
||||
guard !timerManager.isTimerActive else { return }
|
||||
timerManager.startDemoTimer(duration: 1783)
|
||||
startedDemoTimer = true
|
||||
}
|
||||
|
||||
private func stopDemoTimerIfNeeded() {
|
||||
guard startedDemoTimer else { return }
|
||||
TimerManager.shared.forceStopTimer()
|
||||
startedDemoTimer = false
|
||||
}
|
||||
|
||||
private func observeDefaults() {
|
||||
observeKey(.enableLockScreenMediaWidget)
|
||||
observeKey(.enableLockScreenWeatherWidget)
|
||||
observeKey(.enableLockScreenTimerWidget)
|
||||
observeKey(.lockScreenWeatherWidgetStyle)
|
||||
observeKey(.lockScreenWeatherProviderSource)
|
||||
observeKey(.lockScreenWeatherTemperatureUnit)
|
||||
observeKey(.lockScreenWeatherShowsLocation)
|
||||
observeKey(.lockScreenWeatherShowsSunrise)
|
||||
observeKey(.lockScreenWeatherShowsAQI)
|
||||
observeKey(.lockScreenWeatherAQIScale)
|
||||
observeKey(.lockScreenBatteryShowsCharging)
|
||||
observeKey(.lockScreenBatteryShowsChargingPercentage)
|
||||
observeKey(.lockScreenBatteryShowsBatteryGauge)
|
||||
observeKey(.lockScreenBatteryShowsBluetooth)
|
||||
observeKey(.lockScreenBatteryUsesLaptopSymbol)
|
||||
observeKey(.lockScreenWeatherUsesGaugeTint)
|
||||
observeKey(.lockScreenReminderChipStyle)
|
||||
}
|
||||
|
||||
private func observeKey<T: Defaults.Serializable>(_ key: Defaults.Key<T>) {
|
||||
Defaults.publisher(key, options: [])
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.renderPreview()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func mockWeatherSnapshot() -> LockScreenWeatherSnapshot {
|
||||
let unit = Defaults[.lockScreenWeatherTemperatureUnit]
|
||||
let temperatureValue: Double = unit == .celsius ? 21 : 72
|
||||
let temperatureText = "\(Int(round(temperatureValue)))°"
|
||||
let temperatureInfo = LockScreenWeatherSnapshot.TemperatureInfo(
|
||||
current: temperatureValue,
|
||||
minimum: temperatureValue - 5,
|
||||
maximum: temperatureValue + 6,
|
||||
unitSymbol: unit.symbol
|
||||
)
|
||||
|
||||
let chargingInfo: LockScreenWeatherSnapshot.ChargingInfo? = Defaults[.lockScreenBatteryShowsCharging]
|
||||
? LockScreenWeatherSnapshot.ChargingInfo(
|
||||
minutesRemaining: 42,
|
||||
isCharging: true,
|
||||
isPluggedIn: true,
|
||||
batteryLevel: 78
|
||||
)
|
||||
: nil
|
||||
|
||||
let bluetoothInfo: LockScreenWeatherSnapshot.BluetoothInfo? = Defaults[.lockScreenBatteryShowsBluetooth]
|
||||
? LockScreenWeatherSnapshot.BluetoothInfo(
|
||||
deviceName: "AirPods Pro",
|
||||
batteryLevel: 74,
|
||||
iconName: "airpodspro"
|
||||
)
|
||||
: nil
|
||||
|
||||
let batteryInfo: LockScreenWeatherSnapshot.BatteryInfo? = Defaults[.lockScreenBatteryShowsBatteryGauge]
|
||||
? LockScreenWeatherSnapshot.BatteryInfo(
|
||||
batteryLevel: 63,
|
||||
usesLaptopSymbol: Defaults[.lockScreenBatteryUsesLaptopSymbol]
|
||||
)
|
||||
: nil
|
||||
|
||||
let widgetStyle = Defaults[.lockScreenWeatherWidgetStyle]
|
||||
let showLocation = widgetStyle == .inline && Defaults[.lockScreenWeatherShowsLocation]
|
||||
let providerSource = Defaults[.lockScreenWeatherProviderSource]
|
||||
let showsAQI = Defaults[.lockScreenWeatherShowsAQI] && providerSource.supportsAirQuality
|
||||
let scale = Defaults[.lockScreenWeatherAQIScale]
|
||||
let airQualityIndex = scale == .us ? 42 : 28
|
||||
let airQuality = showsAQI
|
||||
? LockScreenWeatherSnapshot.AirQualityInfo(
|
||||
index: airQualityIndex,
|
||||
category: .init(index: airQualityIndex, scale: scale),
|
||||
scale: scale
|
||||
)
|
||||
: nil
|
||||
|
||||
let showsSunrise = Defaults[.lockScreenWeatherShowsSunrise] && widgetStyle == .inline
|
||||
let sunCycle = LockScreenWeatherSnapshot.SunCycleInfo(
|
||||
sunrise: Date().addingTimeInterval(2 * 3600),
|
||||
sunset: Date().addingTimeInterval(9 * 3600)
|
||||
)
|
||||
|
||||
return LockScreenWeatherSnapshot(
|
||||
temperatureText: temperatureText,
|
||||
symbolName: "cloud.sun.fill",
|
||||
description: "Partly Cloudy",
|
||||
locationName: "Cupertino",
|
||||
charging: chargingInfo,
|
||||
bluetooth: bluetoothInfo,
|
||||
battery: batteryInfo,
|
||||
showsLocation: showLocation,
|
||||
airQuality: airQuality,
|
||||
widgetStyle: widgetStyle,
|
||||
showsChargingPercentage: Defaults[.lockScreenBatteryShowsChargingPercentage],
|
||||
temperatureInfo: temperatureInfo,
|
||||
usesGaugeTint: Defaults[.lockScreenWeatherUsesGaugeTint],
|
||||
sunCycle: sunCycle,
|
||||
showsSunrise: showsSunrise
|
||||
)
|
||||
}
|
||||
|
||||
private func ensurePreviewWindow() {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
|
||||
if let window = previewWindow {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
updatePreviewContent()
|
||||
return
|
||||
}
|
||||
|
||||
let frame = screen.frame
|
||||
let window = NSWindow(
|
||||
contentRect: frame,
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.title = "Lock Screen Preview"
|
||||
window.isReleasedWhenClosed = false
|
||||
window.backgroundColor = .clear
|
||||
window.center()
|
||||
|
||||
let scene = LockScreenPreviewScene(
|
||||
screenSize: screen.frame.size,
|
||||
weatherSnapshot: mockWeatherSnapshot(),
|
||||
showsWeather: Defaults[.enableLockScreenWeatherWidget],
|
||||
showsTimer: Defaults[.enableLockScreenTimerWidget],
|
||||
showsMediaPanel: Defaults[.enableLockScreenMediaWidget]
|
||||
)
|
||||
let hosting = NSHostingView(rootView: scene)
|
||||
hosting.frame = NSRect(origin: .zero, size: frame.size)
|
||||
hosting.autoresizingMask = [.width, .height]
|
||||
previewHostingView = hosting
|
||||
window.contentView = hosting
|
||||
|
||||
let delegate = PreviewWindowDelegate { [weak self] in
|
||||
self?.hidePreview()
|
||||
}
|
||||
previewWindowDelegate = delegate
|
||||
window.delegate = delegate
|
||||
|
||||
previewWindow = window
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
private func updatePreviewContent() {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let scene = LockScreenPreviewScene(
|
||||
screenSize: screen.frame.size,
|
||||
weatherSnapshot: mockWeatherSnapshot(),
|
||||
showsWeather: Defaults[.enableLockScreenWeatherWidget],
|
||||
showsTimer: Defaults[.enableLockScreenTimerWidget],
|
||||
showsMediaPanel: Defaults[.enableLockScreenMediaWidget]
|
||||
)
|
||||
|
||||
if let hosting = previewHostingView {
|
||||
hosting.rootView = scene
|
||||
} else if let window = previewWindow {
|
||||
let hosting = NSHostingView(rootView: scene)
|
||||
hosting.frame = NSRect(origin: .zero, size: window.frame.size)
|
||||
hosting.autoresizingMask = [.width, .height]
|
||||
previewHostingView = hosting
|
||||
window.contentView = hosting
|
||||
}
|
||||
}
|
||||
|
||||
private func closePreviewWindow() {
|
||||
previewWindow?.orderOut(nil)
|
||||
}
|
||||
|
||||
private func applyPreviewState() {
|
||||
applyFocusPreviewState()
|
||||
applyCalendarPreviewEvents()
|
||||
}
|
||||
|
||||
private func restorePreviewState() {
|
||||
restoreFocusPreviewState()
|
||||
CalendarManager.shared.setLockScreenPreviewEvents(nil)
|
||||
}
|
||||
|
||||
private func applyFocusPreviewState() {
|
||||
let manager = DoNotDisturbManager.shared
|
||||
if cachedFocusState == nil {
|
||||
cachedFocusState = FocusPreviewState(
|
||||
isActive: manager.isDoNotDisturbActive,
|
||||
name: manager.currentFocusModeName,
|
||||
identifier: manager.currentFocusModeIdentifier
|
||||
)
|
||||
}
|
||||
manager.isDoNotDisturbActive = true
|
||||
manager.currentFocusModeName = "Do Not Disturb"
|
||||
manager.currentFocusModeIdentifier = "com.apple.donotdisturb"
|
||||
}
|
||||
|
||||
private func restoreFocusPreviewState() {
|
||||
guard let cachedFocusState else { return }
|
||||
let manager = DoNotDisturbManager.shared
|
||||
manager.isDoNotDisturbActive = cachedFocusState.isActive
|
||||
manager.currentFocusModeName = cachedFocusState.name
|
||||
manager.currentFocusModeIdentifier = cachedFocusState.identifier
|
||||
self.cachedFocusState = nil
|
||||
}
|
||||
|
||||
private func applyCalendarPreviewEvents() {
|
||||
let now = Date()
|
||||
let reminderCalendar = CalendarModel(
|
||||
id: "preview.reminders",
|
||||
title: "Reminders",
|
||||
color: .systemBlue,
|
||||
isSubscribed: false,
|
||||
isReminder: true
|
||||
)
|
||||
let reminderEvent = EventModel(
|
||||
id: "preview.reminder.pay-rent",
|
||||
start: now.addingTimeInterval(12 * 60),
|
||||
end: now.addingTimeInterval(42 * 60),
|
||||
title: "Pay rent",
|
||||
location: nil,
|
||||
notes: nil,
|
||||
url: nil,
|
||||
isAllDay: false,
|
||||
type: .reminder(completed: false),
|
||||
calendar: reminderCalendar,
|
||||
participants: [],
|
||||
timeZone: TimeZone.current,
|
||||
hasRecurrenceRules: false,
|
||||
priority: .high,
|
||||
conferenceURL: nil
|
||||
)
|
||||
|
||||
let eventCalendar = CalendarModel(
|
||||
id: "preview.calendar",
|
||||
title: "Calendar",
|
||||
color: .systemOrange,
|
||||
isSubscribed: false,
|
||||
isReminder: false
|
||||
)
|
||||
let event = EventModel(
|
||||
id: "preview.event.standup",
|
||||
start: now.addingTimeInterval(60 * 60),
|
||||
end: now.addingTimeInterval(90 * 60),
|
||||
title: "Design Review",
|
||||
location: "Studio",
|
||||
notes: nil,
|
||||
url: nil,
|
||||
isAllDay: false,
|
||||
type: .event(.accepted),
|
||||
calendar: eventCalendar,
|
||||
participants: [],
|
||||
timeZone: TimeZone.current,
|
||||
hasRecurrenceRules: false,
|
||||
priority: nil,
|
||||
conferenceURL: nil
|
||||
)
|
||||
|
||||
CalendarManager.shared.setLockScreenPreviewEvents([reminderEvent, event])
|
||||
}
|
||||
}
|
||||
|
||||
private struct FocusPreviewState {
|
||||
let isActive: Bool
|
||||
let name: String
|
||||
let identifier: String
|
||||
}
|
||||
|
||||
private final class PreviewWindowDelegate: NSObject, NSWindowDelegate {
|
||||
private let onClose: () -> Void
|
||||
|
||||
init(onClose: @escaping () -> Void) {
|
||||
self.onClose = onClose
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
private struct LockScreenPreviewScene: View {
|
||||
let screenSize: CGSize
|
||||
let weatherSnapshot: LockScreenWeatherSnapshot
|
||||
let showsWeather: Bool
|
||||
let showsTimer: Bool
|
||||
let showsMediaPanel: Bool
|
||||
|
||||
@State private var weatherSize: CGSize = .zero
|
||||
@State private var musicSize: CGSize = .zero
|
||||
@State private var inlineBaselineHeight: CGFloat = 0
|
||||
@StateObject private var panelAnimator = LockScreenPanelAnimator()
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
let scale = min(proxy.size.width / screenSize.width, proxy.size.height / screenSize.height)
|
||||
let contentSize = CGSize(width: screenSize.width * scale, height: screenSize.height * scale)
|
||||
let origin = CGPoint(
|
||||
x: (proxy.size.width - contentSize.width) / 2,
|
||||
y: (proxy.size.height - contentSize.height) / 2
|
||||
)
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
WallpaperView()
|
||||
.frame(width: proxy.size.width, height: proxy.size.height)
|
||||
.clipped()
|
||||
|
||||
ZStack(alignment: .topLeading) {
|
||||
if showsMediaPanel {
|
||||
LockScreenMusicPanel(animator: panelAnimator)
|
||||
.fixedSize()
|
||||
.background(PreviewSizeReader(size: $musicSize))
|
||||
.position(centerPoint(for: swiftUIFrame(for: mediaFrame)))
|
||||
.onAppear {
|
||||
panelAnimator.isPresented = true
|
||||
}
|
||||
}
|
||||
|
||||
if showsTimer {
|
||||
LockScreenTimerWidget()
|
||||
.frame(width: timerFrame.width, height: timerFrame.height)
|
||||
.position(centerPoint(for: swiftUIFrame(for: timerFrame)))
|
||||
}
|
||||
|
||||
if showsWeather {
|
||||
LockScreenWeatherWidget(snapshot: weatherSnapshot)
|
||||
.fixedSize()
|
||||
.background(PreviewSizeReader(size: $weatherSize))
|
||||
.position(centerPoint(for: swiftUIFrame(for: weatherFrame)))
|
||||
.onChange(of: weatherSize) { _, newSize in
|
||||
if weatherSnapshot.widgetStyle == .inline {
|
||||
inlineBaselineHeight = max(inlineBaselineHeight, newSize.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize.width, height: screenSize.height, alignment: .topLeading)
|
||||
.scaleEffect(scale, anchor: .topLeading)
|
||||
.offset(x: origin.x, y: origin.y)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
private var mediaFrame: CGRect {
|
||||
let size = musicSize == .zero ? LockScreenMusicPanel.collapsedSize : musicSize
|
||||
let screenFrame = CGRect(origin: .zero, size: screenSize)
|
||||
let originX = screenFrame.midX - (size.width / 2)
|
||||
let baseOriginY = screenFrame.origin.y + (screenFrame.height / 2) - size.height - 32
|
||||
let defaultLowering: CGFloat = -28
|
||||
let userOffset = CGFloat(Defaults[.lockScreenMusicVerticalOffset])
|
||||
let clampedOffset = min(max(userOffset, -160), 160)
|
||||
let originY = baseOriginY + defaultLowering + clampedOffset
|
||||
return CGRect(x: originX, y: originY, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
private var timerFrame: CGRect {
|
||||
let size = LockScreenTimerWidget.preferredSize
|
||||
let screenFrame = CGRect(origin: .zero, size: screenSize)
|
||||
let originX = screenFrame.midX - (size.width / 2)
|
||||
let defaultLowering: CGFloat = -18
|
||||
var baseY = screenFrame.midY + 24 + defaultLowering
|
||||
|
||||
if showsMediaPanel {
|
||||
baseY = mediaFrame.maxY + 28 + defaultLowering
|
||||
}
|
||||
|
||||
let offset = CGFloat(min(max(Defaults[.lockScreenTimerVerticalOffset], -160), 160))
|
||||
var originY = baseY + offset
|
||||
|
||||
if showsMediaPanel {
|
||||
originY = max(originY, mediaFrame.maxY + 12)
|
||||
}
|
||||
|
||||
if showsWeather {
|
||||
originY = min(originY, weatherFrame.minY - size.height - 20)
|
||||
} else {
|
||||
let topLimit = screenFrame.maxY - size.height - 72
|
||||
originY = min(originY, topLimit)
|
||||
}
|
||||
|
||||
let minY = screenFrame.minY + 100
|
||||
originY = max(originY, minY)
|
||||
|
||||
return CGRect(x: originX, y: originY, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
private var weatherFrame: CGRect {
|
||||
let size = weatherSize == .zero ? CGSize(width: 220, height: 60) : weatherSize
|
||||
let screenFrame = CGRect(origin: .zero, size: screenSize)
|
||||
let originX = screenFrame.midX - (size.width / 2)
|
||||
let verticalOffset = screenFrame.height * 0.15
|
||||
let isCircular = weatherSnapshot.widgetStyle == .circular
|
||||
let topMargin: CGFloat = isCircular ? 120 : 48
|
||||
let inlineBaseline = max(inlineBaselineHeight, 80)
|
||||
let positionHeight = weatherSnapshot.widgetStyle == .inline
|
||||
? max(size.height, inlineBaseline)
|
||||
: size.height
|
||||
let maxY = screenFrame.maxY - positionHeight - topMargin
|
||||
let baseY = min(maxY, screenFrame.midY + verticalOffset)
|
||||
let loweredY = baseY - 36
|
||||
|
||||
let inlineLift: CGFloat = weatherSnapshot.widgetStyle == .inline ? 44 : 0
|
||||
let circularDrop: CGFloat = isCircular ? 28 : 0
|
||||
let sizeDropHeight = positionHeight
|
||||
let sizeDrop = max(0, sizeDropHeight - 80) * 0.35
|
||||
let userOffset = CGFloat(Defaults[.lockScreenWeatherVerticalOffset])
|
||||
let clampedOffset = min(max(userOffset, -160), 160)
|
||||
let adjustedY = loweredY + inlineLift + clampedOffset - circularDrop - sizeDrop
|
||||
let upperClampedY = min(maxY, adjustedY)
|
||||
let clampedY = max(screenFrame.minY + 80, upperClampedY)
|
||||
|
||||
return CGRect(x: originX, y: clampedY, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
private func swiftUIFrame(for appKitFrame: CGRect) -> CGRect {
|
||||
let originY = screenSize.height - appKitFrame.maxY
|
||||
return CGRect(x: appKitFrame.origin.x, y: originY, width: appKitFrame.width, height: appKitFrame.height)
|
||||
}
|
||||
|
||||
private func centerPoint(for frame: CGRect) -> CGPoint {
|
||||
CGPoint(x: frame.midX, y: frame.midY)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PreviewSizeReader: View {
|
||||
@Binding var size: CGSize
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear { size = proxy.size }
|
||||
.onChange(of: proxy.size) { _, newSize in
|
||||
size = newSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WallpaperView: View {
|
||||
@State private var wallpaperImage: NSImage?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let image = wallpaperImage {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
Color.black
|
||||
Text("Loading...")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.onAppear(perform: loadWallpaper)
|
||||
}
|
||||
|
||||
func loadWallpaper() {
|
||||
if let url = Bundle.main.url(forResource: "desktop", withExtension: "jpeg"),
|
||||
let image = NSImage(contentsOf: url),
|
||||
image.size.width > 0,
|
||||
image.size.height > 0 {
|
||||
self.wallpaperImage = image
|
||||
return
|
||||
}
|
||||
|
||||
if let image = NSImage(named: "desktop"),
|
||||
image.size.width > 0,
|
||||
image.size.height > 0 {
|
||||
self.wallpaperImage = image
|
||||
return
|
||||
}
|
||||
|
||||
if let screen = NSScreen.main,
|
||||
let url = NSWorkspace.shared.desktopImageURL(for: screen),
|
||||
let image = NSImage(contentsOf: url),
|
||||
image.size.width > 0,
|
||||
image.size.height > 0 {
|
||||
self.wallpaperImage = image
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue