mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Add Repositionable Widgets modifier
This commit is contained in:
parent
0a6b4a576b
commit
bd23b5f210
1 changed files with 79 additions and 35 deletions
|
|
@ -1813,108 +1813,152 @@ private struct LockScreenPositioningPreview: View {
|
||||||
@State private var isWeatherDragging = false
|
@State private var isWeatherDragging = false
|
||||||
@State private var isMusicDragging = false
|
@State private var isMusicDragging = false
|
||||||
|
|
||||||
|
private let offsetRange: ClosedRange<Double> = -160...160
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let centerX = geometry.size.width / 2
|
let screenPadding: CGFloat = 26
|
||||||
let weatherBaseY = geometry.size.height * 0.34
|
let screenCornerRadius: CGFloat = 28
|
||||||
let musicBaseY = geometry.size.height * 0.70
|
let screenRect = CGRect(
|
||||||
|
x: screenPadding,
|
||||||
|
y: screenPadding,
|
||||||
|
width: geometry.size.width - (screenPadding * 2),
|
||||||
|
height: geometry.size.height - (screenPadding * 2)
|
||||||
|
)
|
||||||
|
let centerX = screenRect.midX
|
||||||
|
let weatherBaseY = screenRect.minY + (screenRect.height * 0.28)
|
||||||
|
let musicBaseY = screenRect.minY + (screenRect.height * 0.68)
|
||||||
|
let weatherSize = CGSize(width: screenRect.width * 0.42, height: screenRect.height * 0.22)
|
||||||
|
let musicSize = CGSize(width: screenRect.width * 0.56, height: screenRect.height * 0.34)
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
RoundedRectangle(cornerRadius: screenCornerRadius, style: .continuous)
|
||||||
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.6))
|
.fill(Color(nsColor: .windowBackgroundColor).opacity(0.55))
|
||||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
.frame(width: screenRect.width, height: screenRect.height)
|
||||||
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: screenCornerRadius, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.12), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.shadow(color: Color.black.opacity(0.22), radius: 20, x: 0, y: 18)
|
||||||
|
.position(x: screenRect.midX, y: screenRect.midY)
|
||||||
|
|
||||||
weatherPanel
|
weatherPanel(size: weatherSize)
|
||||||
.position(x: centerX, y: weatherBaseY - CGFloat(weatherOffset))
|
.position(x: centerX, y: weatherBaseY - CGFloat(weatherOffset))
|
||||||
.gesture(weatherDragGesture)
|
.gesture(weatherDragGesture(in: screenRect, baseY: weatherBaseY, panelSize: weatherSize))
|
||||||
|
|
||||||
musicPanel
|
musicPanel(size: musicSize)
|
||||||
.position(x: centerX, y: musicBaseY - CGFloat(musicOffset))
|
.position(x: centerX, y: musicBaseY - CGFloat(musicOffset))
|
||||||
.gesture(musicDragGesture)
|
.gesture(musicDragGesture(in: screenRect, baseY: musicBaseY, panelSize: musicSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: weatherOffset)
|
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: weatherOffset)
|
||||||
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicOffset)
|
.animation(.interactiveSpring(response: 0.3, dampingFraction: 0.82), value: musicOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var weatherPanel: some View {
|
private func weatherPanel(size: CGSize) -> some View {
|
||||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(
|
.fill(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: [Color.blue.opacity(0.72), Color.blue.opacity(0.48)],
|
colors: [Color.blue.opacity(0.78), Color.blue.opacity(0.52)],
|
||||||
startPoint: .topLeading,
|
startPoint: .topLeading,
|
||||||
endPoint: .bottomTrailing
|
endPoint: .bottomTrailing
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.frame(width: 220, height: 88)
|
.frame(width: size.width, height: size.height)
|
||||||
.overlay(alignment: .leading) {
|
.overlay(alignment: .leading) {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Label("Weather", systemImage: "cloud.sun.fill")
|
Label("Weather", systemImage: "cloud.sun.fill")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
Text("Inline snapshot preview")
|
Text("Inline snapshot preview")
|
||||||
.font(.system(size: 11, weight: .medium))
|
.font(.system(size: 10, weight: .medium))
|
||||||
.foregroundStyle(Color.white.opacity(0.7))
|
.foregroundStyle(Color.white.opacity(0.72))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
}
|
||||||
|
.shadow(color: Color.blue.opacity(0.22), radius: 10, x: 0, y: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func musicPanel(size: CGSize) -> some View {
|
||||||
|
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.purple.opacity(0.68), Color.pink.opacity(0.5)],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.frame(width: size.width, height: size.height)
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Label("Media", systemImage: "play.square.stack")
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
Text("Lock screen panel preview")
|
||||||
|
.font(.system(size: 10, weight: .medium))
|
||||||
|
.foregroundStyle(Color.white.opacity(0.72))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
}
|
}
|
||||||
.shadow(color: Color.blue.opacity(0.25), radius: 12, x: 0, y: 8)
|
.shadow(color: Color.purple.opacity(0.24), radius: 12, x: 0, y: 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var musicPanel: some View {
|
private func weatherDragGesture(in screenRect: CGRect, baseY: CGFloat, panelSize: CGSize) -> some Gesture {
|
||||||
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)
|
DragGesture(minimumDistance: 0)
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
if !isWeatherDragging {
|
if !isWeatherDragging {
|
||||||
isWeatherDragging = true
|
isWeatherDragging = true
|
||||||
weatherStartOffset = weatherOffset
|
weatherStartOffset = weatherOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
let proposed = weatherStartOffset - Double(value.translation.height)
|
let proposed = weatherStartOffset - Double(value.translation.height)
|
||||||
weatherOffset = proposed
|
weatherOffset = clampedOffset(
|
||||||
|
proposed,
|
||||||
|
baseCenterY: baseY,
|
||||||
|
panelHeight: panelSize.height,
|
||||||
|
screenRect: screenRect
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
isWeatherDragging = false
|
isWeatherDragging = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var musicDragGesture: some Gesture {
|
private func musicDragGesture(in screenRect: CGRect, baseY: CGFloat, panelSize: CGSize) -> some Gesture {
|
||||||
DragGesture(minimumDistance: 0)
|
DragGesture(minimumDistance: 0)
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
if !isMusicDragging {
|
if !isMusicDragging {
|
||||||
isMusicDragging = true
|
isMusicDragging = true
|
||||||
musicStartOffset = musicOffset
|
musicStartOffset = musicOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
let proposed = musicStartOffset - Double(value.translation.height)
|
let proposed = musicStartOffset - Double(value.translation.height)
|
||||||
musicOffset = proposed
|
musicOffset = clampedOffset(
|
||||||
|
proposed,
|
||||||
|
baseCenterY: baseY,
|
||||||
|
panelHeight: panelSize.height,
|
||||||
|
screenRect: screenRect
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.onEnded { _ in
|
.onEnded { _ in
|
||||||
isMusicDragging = false
|
isMusicDragging = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func clampedOffset(
|
||||||
|
_ proposed: Double,
|
||||||
|
baseCenterY: CGFloat,
|
||||||
|
panelHeight: CGFloat,
|
||||||
|
screenRect: CGRect
|
||||||
|
) -> Double {
|
||||||
|
let halfHeight = panelHeight / 2
|
||||||
|
let minCenterY = screenRect.minY + halfHeight
|
||||||
|
let maxCenterY = screenRect.maxY - halfHeight
|
||||||
|
let proposedCenter = baseCenterY - CGFloat(proposed)
|
||||||
|
let clampedCenter = min(max(proposedCenter, minCenterY), maxCenterY)
|
||||||
|
let derivedOffset = Double(baseCenterY - clampedCenter)
|
||||||
|
return min(max(derivedOffset, offsetRange.lowerBound), offsetRange.upperBound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Shortcuts: View {
|
struct Shortcuts: View {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue