mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Added positioning mocks
This commit is contained in:
parent
ea2706ad96
commit
0a6b4a576b
5 changed files with 310 additions and 7 deletions
|
|
@ -751,6 +751,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"%@ Offset" : {
|
||||
"comment" : "A control within the lock screen settings that allows the user to adjust the offset for a specific dimension (e.g. width or height). The argument is the name of the dimension, and the second argument is",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%@ pt" : {
|
||||
"comment" : "A text view displaying the current offset value for a specific lock screen control. The argument is the name of the control.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%@°" : {
|
||||
"comment" : "A label inside the temperature gauge that shows the current temperature. The value is shown in degrees Celsius (or Fahrenheit, if specified in the snapshot).",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
|
@ -7052,6 +7060,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Changes apply instantly while the widgets are visible." : {
|
||||
|
||||
},
|
||||
"Drop files here" : {
|
||||
"localizations" : {
|
||||
|
|
@ -10225,6 +10236,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Inline snapshot preview" : {
|
||||
|
||||
},
|
||||
"Install updates automatically" : {
|
||||
"extractionState" : "stale",
|
||||
|
|
@ -10587,6 +10601,14 @@
|
|||
},
|
||||
"Lock screen material" : {
|
||||
|
||||
},
|
||||
"Lock screen panel preview" : {
|
||||
"comment" : "A description of the content displayed in the lock screen panel.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Lock Screen Positioning" : {
|
||||
"comment" : "The header text for the \"Lock Screen Positioning\" section in the settings view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Lock Screen Reminder Widget" : {
|
||||
"comment" : "A section header for the lock screen reminder widget settings.",
|
||||
|
|
@ -19766,6 +19788,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Weather" : {
|
||||
"comment" : "A label and its accompanying text displayed in a lock screen panel.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Weather data provider" : {
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1436,6 +1436,8 @@ struct Appearance: View {
|
|||
Text("General")
|
||||
}
|
||||
|
||||
LockScreenPositioningControls()
|
||||
|
||||
Section {
|
||||
Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram)
|
||||
Defaults
|
||||
|
|
@ -1693,6 +1695,228 @@ struct Appearance: View {
|
|||
}
|
||||
}
|
||||
|
||||
private struct LockScreenPositioningControls: View {
|
||||
@Default(.lockScreenWeatherVerticalOffset) private var weatherOffset
|
||||
@Default(.lockScreenMusicVerticalOffset) private var musicOffset
|
||||
private let offsetRange: ClosedRange<Double> = -160...160
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
let weatherBinding = Binding<Double>(
|
||||
get: { weatherOffset },
|
||||
set: { newValue in
|
||||
let clampedValue = clamp(newValue)
|
||||
if weatherOffset != clampedValue {
|
||||
weatherOffset = clampedValue
|
||||
}
|
||||
propagateWeatherOffsetChange(animated: false)
|
||||
}
|
||||
)
|
||||
|
||||
let musicBinding = Binding<Double>(
|
||||
get: { musicOffset },
|
||||
set: { newValue in
|
||||
let clampedValue = clamp(newValue)
|
||||
if musicOffset != clampedValue {
|
||||
musicOffset = clampedValue
|
||||
}
|
||||
propagateMusicOffsetChange(animated: false)
|
||||
}
|
||||
)
|
||||
|
||||
LockScreenPositioningPreview(weatherOffset: weatherBinding, musicOffset: musicBinding)
|
||||
.frame(height: 260)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
HStack(alignment: .top, spacing: 24) {
|
||||
offsetColumn(
|
||||
title: "Weather",
|
||||
value: weatherOffset,
|
||||
resetTitle: "Reset Weather",
|
||||
resetAction: resetWeatherOffset
|
||||
)
|
||||
|
||||
Divider()
|
||||
.frame(height: 64)
|
||||
|
||||
offsetColumn(
|
||||
title: "Music",
|
||||
value: musicOffset,
|
||||
resetTitle: "Reset Music",
|
||||
resetAction: resetMusicOffset
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
} header: {
|
||||
Text("Lock Screen Positioning")
|
||||
} footer: {
|
||||
Text("Drag the previews to adjust vertical placement. Positive values lift the panel; negative values lower it. Changes apply instantly while the widgets are visible.")
|
||||
.textCase(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func clamp(_ value: Double) -> Double {
|
||||
min(max(value, offsetRange.lowerBound), offsetRange.upperBound)
|
||||
}
|
||||
|
||||
private func resetWeatherOffset() {
|
||||
weatherOffset = 0
|
||||
propagateWeatherOffsetChange(animated: true)
|
||||
}
|
||||
|
||||
private func resetMusicOffset() {
|
||||
musicOffset = 0
|
||||
propagateMusicOffsetChange(animated: true)
|
||||
}
|
||||
|
||||
private func propagateWeatherOffsetChange(animated: Bool) {
|
||||
Task { @MainActor in
|
||||
LockScreenWeatherPanelManager.shared.refreshPositionForOffsets(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private func propagateMusicOffsetChange(animated: Bool) {
|
||||
Task { @MainActor in
|
||||
LockScreenPanelManager.shared.applyOffsetAdjustment(animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func offsetColumn(title: String, value: Double, resetTitle: String, resetAction: @escaping () -> Void) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("\(title) Offset")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
|
||||
Text("\(formattedPoints(value)) pt")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Button(resetTitle) {
|
||||
resetAction()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedPoints(_ value: Double) -> String {
|
||||
String(format: "%+.0f", value)
|
||||
}
|
||||
}
|
||||
|
||||
private struct LockScreenPositioningPreview: View {
|
||||
@Binding var weatherOffset: Double
|
||||
@Binding var musicOffset: Double
|
||||
|
||||
@State private var weatherStartOffset: Double = 0
|
||||
@State private var musicStartOffset: Double = 0
|
||||
@State private var isWeatherDragging = false
|
||||
@State private var isMusicDragging = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let centerX = geometry.size.width / 2
|
||||
let weatherBaseY = geometry.size.height * 0.34
|
||||
let musicBaseY = geometry.size.height * 0.70
|
||||
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.6))
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
||||
|
||||
weatherPanel
|
||||
.position(x: centerX, y: weatherBaseY - CGFloat(weatherOffset))
|
||||
.gesture(weatherDragGesture)
|
||||
|
||||
musicPanel
|
||||
.position(x: centerX, y: musicBaseY - CGFloat(musicOffset))
|
||||
.gesture(musicDragGesture)
|
||||
}
|
||||
}
|
||||
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: weatherOffset)
|
||||
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicOffset)
|
||||
}
|
||||
|
||||
private var weatherPanel: some View {
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color.blue.opacity(0.72), Color.blue.opacity(0.48)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 220, height: 88)
|
||||
.overlay(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Label("Weather", systemImage: "cloud.sun.fill")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(Color.white)
|
||||
Text("Inline snapshot preview")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(Color.white.opacity(0.7))
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
}
|
||||
.shadow(color: Color.blue.opacity(0.25), radius: 12, x: 0, y: 8)
|
||||
}
|
||||
|
||||
private var musicPanel: some View {
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color.purple.opacity(0.68), Color.pink.opacity(0.52)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 280, height: 132)
|
||||
.overlay(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Media", systemImage: "play.square.stack")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(Color.white)
|
||||
Text("Lock screen panel preview")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(Color.white.opacity(0.7))
|
||||
}
|
||||
.padding(.horizontal, 22)
|
||||
}
|
||||
.shadow(color: Color.purple.opacity(0.3), radius: 16, x: 0, y: 10)
|
||||
}
|
||||
|
||||
private var weatherDragGesture: some Gesture {
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
if !isWeatherDragging {
|
||||
isWeatherDragging = true
|
||||
weatherStartOffset = weatherOffset
|
||||
}
|
||||
let proposed = weatherStartOffset - Double(value.translation.height)
|
||||
weatherOffset = proposed
|
||||
}
|
||||
.onEnded { _ in
|
||||
isWeatherDragging = false
|
||||
}
|
||||
}
|
||||
|
||||
private var musicDragGesture: some Gesture {
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { value in
|
||||
if !isMusicDragging {
|
||||
isMusicDragging = true
|
||||
musicStartOffset = musicOffset
|
||||
}
|
||||
let proposed = musicStartOffset - Double(value.translation.height)
|
||||
musicOffset = proposed
|
||||
}
|
||||
.onEnded { _ in
|
||||
isMusicDragging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Shortcuts: View {
|
||||
@Default(.enableTimerFeature) var enableTimerFeature
|
||||
@Default(.enableClipboardManager) var enableClipboardManager
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ class LockScreenPanelManager {
|
|||
private var panelWindow: NSWindow?
|
||||
private var hasDelegated = false
|
||||
private var collapsedFrame: NSRect?
|
||||
private var isPanelExpanded = false
|
||||
private var currentAdditionalHeight: CGFloat = 0
|
||||
private let collapsedPanelCornerRadius: CGFloat = 28
|
||||
private let expandedPanelCornerRadius: CGFloat = 52
|
||||
private(set) var latestFrame: NSRect?
|
||||
|
|
@ -53,13 +55,11 @@ class LockScreenPanelManager {
|
|||
return
|
||||
}
|
||||
|
||||
let collapsedSize = LockScreenMusicPanel.collapsedSize
|
||||
let screenFrame = screen.frame
|
||||
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
|
||||
let originX = centerX - (collapsedSize.width / 2)
|
||||
let originY = screenFrame.origin.y + (screenFrame.height / 2) - collapsedSize.height - 32
|
||||
let targetFrame = NSRect(x: originX, y: originY, width: collapsedSize.width, height: collapsedSize.height)
|
||||
let targetFrame = collapsedFrame(for: screenFrame)
|
||||
collapsedFrame = targetFrame
|
||||
isPanelExpanded = false
|
||||
currentAdditionalHeight = 0
|
||||
|
||||
let window: NSWindow
|
||||
|
||||
|
|
@ -152,6 +152,19 @@ class LockScreenPanelManager {
|
|||
} else {
|
||||
window.contentView?.layer?.cornerRadius = targetRadius
|
||||
}
|
||||
|
||||
isPanelExpanded = expanded
|
||||
currentAdditionalHeight = additionalHeight
|
||||
}
|
||||
|
||||
func applyOffsetAdjustment(animated: Bool = true) {
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let screenFrame = screen.frame
|
||||
let newCollapsed = collapsedFrame(for: screenFrame)
|
||||
collapsedFrame = newCollapsed
|
||||
|
||||
guard panelWindow != nil else { return }
|
||||
updatePanelSize(expanded: isPanelExpanded, additionalHeight: currentAdditionalHeight, animated: animated)
|
||||
}
|
||||
|
||||
func hidePanel() {
|
||||
|
|
@ -177,4 +190,14 @@ class LockScreenPanelManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func collapsedFrame(for screenFrame: NSRect) -> NSRect {
|
||||
let collapsedSize = LockScreenMusicPanel.collapsedSize
|
||||
let originX = screenFrame.midX - (collapsedSize.width / 2)
|
||||
let baseOriginY = screenFrame.origin.y + (screenFrame.height / 2) - collapsedSize.height - 32
|
||||
let userOffset = CGFloat(Defaults[.lockScreenMusicVerticalOffset])
|
||||
let clampedOffset = min(max(userOffset, -160), 160)
|
||||
let originY = baseOriginY + clampedOffset
|
||||
return NSRect(x: originX, y: originY, width: collapsedSize.width, height: collapsedSize.height)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import AppKit
|
||||
import SwiftUI
|
||||
import SkyLightWindow
|
||||
import Defaults
|
||||
import QuartzCore
|
||||
|
||||
@MainActor
|
||||
final class LockScreenWeatherPanelManager {
|
||||
|
|
@ -9,6 +11,8 @@ final class LockScreenWeatherPanelManager {
|
|||
private var window: NSWindow?
|
||||
private var hasDelegated = false
|
||||
private(set) var latestFrame: NSRect?
|
||||
private var lastSnapshot: LockScreenWeatherSnapshot?
|
||||
private var lastContentSize: CGSize?
|
||||
|
||||
private init() {}
|
||||
|
||||
|
|
@ -25,6 +29,8 @@ final class LockScreenWeatherPanelManager {
|
|||
window.orderOut(nil)
|
||||
window.contentView = nil
|
||||
latestFrame = nil
|
||||
lastSnapshot = nil
|
||||
lastContentSize = nil
|
||||
}
|
||||
|
||||
private func render(snapshot: LockScreenWeatherSnapshot, makeVisible: Bool) {
|
||||
|
|
@ -43,6 +49,8 @@ final class LockScreenWeatherPanelManager {
|
|||
window.setFrame(targetFrame, display: true)
|
||||
latestFrame = targetFrame
|
||||
window.contentView = hostingView
|
||||
lastSnapshot = snapshot
|
||||
lastContentSize = fittingSize
|
||||
|
||||
if makeVisible {
|
||||
window.orderFrontRegardless()
|
||||
|
|
@ -86,10 +94,30 @@ final class LockScreenWeatherPanelManager {
|
|||
let baseY = min(maxY, screenFrame.midY + verticalOffset)
|
||||
let loweredY = baseY - 36
|
||||
|
||||
let inlineLift: CGFloat = snapshot.widgetStyle == .inline ? 28 : 0
|
||||
let adjustedY = loweredY + inlineLift
|
||||
let inlineLift: CGFloat = snapshot.widgetStyle == .inline ? 44 : 0
|
||||
let userOffset = CGFloat(Defaults[.lockScreenWeatherVerticalOffset])
|
||||
let clampedOffset = min(max(userOffset, -160), 160)
|
||||
let adjustedY = loweredY + inlineLift + clampedOffset
|
||||
let upperClampedY = min(maxY, adjustedY)
|
||||
let clampedY = max(screenFrame.minY + 80, upperClampedY)
|
||||
return NSRect(x: originX, y: clampedY, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
func refreshPositionForOffsets(animated: Bool = true) {
|
||||
guard let window, let snapshot = lastSnapshot else { return }
|
||||
guard let screen = NSScreen.main else { return }
|
||||
let size = lastContentSize ?? window.frame.size
|
||||
let targetFrame = frame(for: size, snapshot: snapshot, on: screen)
|
||||
latestFrame = targetFrame
|
||||
|
||||
if animated {
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = 0.22
|
||||
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
window.animator().setFrame(targetFrame, display: true)
|
||||
}
|
||||
} else {
|
||||
window.setFrame(targetFrame, display: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -438,6 +438,8 @@ extension Defaults.Keys {
|
|||
static let lockScreenWeatherAQIScale = Key<LockScreenWeatherAirQualityScale>("lockScreenWeatherAQIScale", default: .us)
|
||||
static let lockScreenWeatherUsesGaugeTint = Key<Bool>("lockScreenWeatherUsesGaugeTint", default: false)
|
||||
static let lockScreenWeatherProviderSource = Key<LockScreenWeatherProviderSource>("lockScreenWeatherProviderSource", default: .openMeteo)
|
||||
static let lockScreenWeatherVerticalOffset = Key<Double>("lockScreenWeatherVerticalOffset", default: 0)
|
||||
static let lockScreenMusicVerticalOffset = Key<Double>("lockScreenMusicVerticalOffset", default: 0)
|
||||
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
||||
static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
|
||||
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue