mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Temporary push: Will fix the animation later
This commit is contained in:
parent
ba27aa3d0f
commit
b7cf4dcf0c
3 changed files with 112 additions and 64 deletions
|
|
@ -215,14 +215,14 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
.onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in
|
.onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isStatsPopoverActive && !vm.isMediaOutputPopoverActive {
|
if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isStatsPopoverActive && !vm.isMediaOutputPopoverActive && !vm.isReminderPopoverActive {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: vm.isStatsPopoverActive) { _, newPopoverState in
|
.onChange(of: vm.isStatsPopoverActive) { _, newPopoverState in
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isMediaOutputPopoverActive {
|
if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isMediaOutputPopoverActive && !vm.isReminderPopoverActive {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +230,7 @@ struct ContentView: View {
|
||||||
.onChange(of: vm.shouldRecheckHover) { _, _ in
|
.onChange(of: vm.shouldRecheckHover) { _, _ in
|
||||||
// Recheck hover state when popovers are closed
|
// Recheck hover state when popovers are closed
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isStatsPopoverActive && !vm.isMediaOutputPopoverActive && !isHovering {
|
if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isStatsPopoverActive && !vm.isMediaOutputPopoverActive && !vm.isReminderPopoverActive && !isHovering {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -831,7 +831,8 @@ struct ContentView: View {
|
||||||
vm.isColorPickerPopoverActive ||
|
vm.isColorPickerPopoverActive ||
|
||||||
vm.isStatsPopoverActive ||
|
vm.isStatsPopoverActive ||
|
||||||
vm.isTimerPopoverActive ||
|
vm.isTimerPopoverActive ||
|
||||||
vm.isMediaOutputPopoverActive
|
vm.isMediaOutputPopoverActive ||
|
||||||
|
vm.isReminderPopoverActive
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to prevent rapid haptic feedback
|
// Helper to prevent rapid haptic feedback
|
||||||
|
|
|
||||||
|
|
@ -86,17 +86,17 @@ struct MinimalisticMusicPlayerView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var reminderList: some View {
|
private var reminderList: some View {
|
||||||
MinimalisticReminderEventListView(reminders: reminderEntries, currentDate: reminderManager.currentDate)
|
MinimalisticReminderEventListView(reminders: reminderEntries)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.frame(height: reminderListHeight, alignment: .top)
|
.frame(height: reminderListHeight, alignment: .top)
|
||||||
.opacity(shouldShowReminderList ? 1 : 0)
|
.opacity(shouldShowReminderList ? 1 : 0)
|
||||||
.animation(.easeInOut(duration: 0.18), value: shouldShowReminderList)
|
.animation(.easeInOut(duration: 0.18), value: shouldShowReminderList)
|
||||||
|
.environmentObject(vm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private struct MinimalisticReminderEventListView: View {
|
private struct MinimalisticReminderEventListView: View {
|
||||||
let reminders: [ReminderLiveActivityManager.ReminderEntry]
|
let reminders: [ReminderLiveActivityManager.ReminderEntry]
|
||||||
let currentDate: Date
|
|
||||||
|
|
||||||
private let textFont = Font.system(size: 13, weight: .semibold)
|
private let textFont = Font.system(size: 13, weight: .semibold)
|
||||||
private let separatorSpacing: CGFloat = 10
|
private let separatorSpacing: CGFloat = 10
|
||||||
|
|
@ -104,7 +104,7 @@ private struct MinimalisticReminderEventListView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: ReminderLiveActivityManager.listRowSpacing) {
|
VStack(alignment: .leading, spacing: ReminderLiveActivityManager.listRowSpacing) {
|
||||||
ForEach(reminders) { entry in
|
ForEach(reminders) { entry in
|
||||||
MinimalisticReminderEventRow(entry: entry, now: currentDate, textFont: textFont, separatorSpacing: separatorSpacing)
|
MinimalisticReminderEventRow(entry: entry, textFont: textFont, separatorSpacing: separatorSpacing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, ReminderLiveActivityManager.listTopPadding)
|
.padding(.top, ReminderLiveActivityManager.listTopPadding)
|
||||||
|
|
@ -113,26 +113,44 @@ private struct MinimalisticReminderEventListView: View {
|
||||||
|
|
||||||
private struct MinimalisticReminderEventRow: View {
|
private struct MinimalisticReminderEventRow: View {
|
||||||
let entry: ReminderLiveActivityManager.ReminderEntry
|
let entry: ReminderLiveActivityManager.ReminderEntry
|
||||||
let now: Date
|
|
||||||
let textFont: Font
|
let textFont: Font
|
||||||
let separatorSpacing: CGFloat
|
let separatorSpacing: CGFloat
|
||||||
|
|
||||||
@State private var isDetailsPresented = false
|
@EnvironmentObject private var vm: DynamicIslandViewModel
|
||||||
|
@State private var didCopyLink = false
|
||||||
|
@State private var copyResetToken: UUID?
|
||||||
|
@State private var isDetailsPopoverPresented = false
|
||||||
|
@State private var isHoveringDetailsPopover = false
|
||||||
|
|
||||||
private let ringDiameter: CGFloat = 20
|
private let indicatorHeight: CGFloat = 20
|
||||||
private let ringLineWidth: CGFloat = 3
|
|
||||||
|
private static let timeFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateStyle = .none
|
||||||
|
formatter.timeStyle = .short
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: separatorSpacing) {
|
HStack(spacing: separatorSpacing) {
|
||||||
RoundedRectangle(cornerRadius: 3)
|
RoundedRectangle(cornerRadius: 3)
|
||||||
.fill(eventColor)
|
.fill(eventColor)
|
||||||
.frame(width: 8, height: ringDiameter)
|
.frame(width: 8, height: indicatorHeight)
|
||||||
|
|
||||||
Text(entry.event.title)
|
HStack(spacing: 6) {
|
||||||
.font(textFont)
|
Text(entry.event.title)
|
||||||
.foregroundStyle(Color.white)
|
.font(textFont)
|
||||||
.lineLimit(1)
|
.foregroundStyle(Color.white)
|
||||||
.layoutPriority(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
|
if let timeText {
|
||||||
|
Text(timeText)
|
||||||
|
.font(.system(size: 12, weight: .regular))
|
||||||
|
.foregroundStyle(Color.white.opacity(0.6))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layoutPriority(1)
|
||||||
|
|
||||||
Spacer(minLength: 12)
|
Spacer(minLength: 12)
|
||||||
|
|
||||||
|
|
@ -140,10 +158,13 @@ private struct MinimalisticReminderEventRow: View {
|
||||||
if let url = linkURL {
|
if let url = linkURL {
|
||||||
Button {
|
Button {
|
||||||
copyToClipboard(url: url)
|
copyToClipboard(url: url)
|
||||||
|
triggerCopyFeedback()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "link")
|
Image(systemName: didCopyLink ? "checkmark.circle.fill" : "link")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
.foregroundStyle(Color.white.opacity(0.85))
|
.foregroundStyle(didCopyLink ? Color.green : Color.white.opacity(0.85))
|
||||||
|
.symbolRenderingMode(.monochrome)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: didCopyLink)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.help("Copy event link")
|
.help("Copy event link")
|
||||||
|
|
@ -151,50 +172,52 @@ private struct MinimalisticReminderEventRow: View {
|
||||||
|
|
||||||
if hasDetails {
|
if hasDetails {
|
||||||
Button {
|
Button {
|
||||||
isDetailsPresented.toggle()
|
isDetailsPopoverPresented.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "info.circle")
|
Image(systemName: "info.circle")
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 14, weight: .semibold))
|
||||||
.foregroundStyle(Color.white.opacity(0.85))
|
.foregroundStyle(Color.white.opacity(0.85))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.popover(isPresented: $isDetailsPresented, arrowEdge: .top) {
|
.popover(isPresented: $isDetailsPopoverPresented, arrowEdge: .top) {
|
||||||
MinimalisticReminderDetailsView(entry: entry, linkURL: linkURL)
|
MinimalisticReminderDetailsView(
|
||||||
|
entry: entry,
|
||||||
|
linkURL: linkURL,
|
||||||
|
onHoverChanged: { hovering in
|
||||||
|
isHoveringDetailsPopover = hovering
|
||||||
|
updatePopoverActivity()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onDisappear {
|
||||||
|
isHoveringDetailsPopover = false
|
||||||
|
updatePopoverActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: isDetailsPopoverPresented) { _, presented in
|
||||||
|
if !presented {
|
||||||
|
isHoveringDetailsPopover = false
|
||||||
|
updatePopoverActivity()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReminderProgressRing(progress: progress, accent: accentColor, diameter: ringDiameter, lineWidth: ringLineWidth)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: ReminderLiveActivityManager.listRowHeight)
|
.frame(height: ReminderLiveActivityManager.listRowHeight)
|
||||||
}
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
private var progress: Double {
|
openInCalendar()
|
||||||
guard entry.leadTime > 0 else { return 1 }
|
}
|
||||||
let remaining = max(entry.event.start.timeIntervalSince(now), 0)
|
.onDisappear {
|
||||||
let elapsed = entry.leadTime - remaining
|
copyResetToken = nil
|
||||||
let ratio = elapsed / entry.leadTime
|
didCopyLink = false
|
||||||
return min(max(ratio, 0), 1)
|
vm.isReminderPopoverActive = false
|
||||||
}
|
|
||||||
|
|
||||||
private var accentColor: Color {
|
|
||||||
if isCritical {
|
|
||||||
return .red
|
|
||||||
}
|
}
|
||||||
return eventColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var eventColor: Color {
|
private var eventColor: Color {
|
||||||
Color(nsColor: entry.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
Color(nsColor: entry.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isCritical: Bool {
|
|
||||||
let window = TimeInterval(Defaults[.reminderSneakPeekDuration])
|
|
||||||
guard window > 0 else { return false }
|
|
||||||
let remaining = entry.event.start.timeIntervalSince(now)
|
|
||||||
return remaining > 0 && remaining <= window
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hasDetails: Bool {
|
private var hasDetails: Bool {
|
||||||
let location = entry.event.location?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let location = entry.event.location?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
let notes = entry.event.notes?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let notes = entry.event.notes?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
|
@ -205,6 +228,35 @@ private struct MinimalisticReminderEventRow: View {
|
||||||
entry.event.url ?? entry.event.calendarAppURL()
|
entry.event.url ?? entry.event.calendarAppURL()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var timeText: String? {
|
||||||
|
Self.timeFormatter.string(from: entry.event.start)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePopoverActivity() {
|
||||||
|
vm.isReminderPopoverActive = isDetailsPopoverPresented && isHoveringDetailsPopover
|
||||||
|
}
|
||||||
|
|
||||||
|
private func triggerCopyFeedback() {
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.65)) {
|
||||||
|
didCopyLink = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = UUID()
|
||||||
|
copyResetToken = token
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [token] in
|
||||||
|
guard copyResetToken == token else { return }
|
||||||
|
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
didCopyLink = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if copyResetToken == token {
|
||||||
|
copyResetToken = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func copyToClipboard(url: URL) {
|
private func copyToClipboard(url: URL) {
|
||||||
#if canImport(AppKit)
|
#if canImport(AppKit)
|
||||||
let pasteboard = NSPasteboard.general
|
let pasteboard = NSPasteboard.general
|
||||||
|
|
@ -212,31 +264,19 @@ private struct MinimalisticReminderEventRow: View {
|
||||||
pasteboard.setString(url.absoluteString, forType: .string)
|
pasteboard.setString(url.absoluteString, forType: .string)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private struct ReminderProgressRing: View {
|
private func openInCalendar() {
|
||||||
let progress: Double
|
#if canImport(AppKit)
|
||||||
let accent: Color
|
guard let url = linkURL else { return }
|
||||||
let diameter: CGFloat
|
NSWorkspace.shared.open(url)
|
||||||
let lineWidth: CGFloat
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.stroke(Color.white.opacity(0.18), lineWidth: lineWidth)
|
|
||||||
Circle()
|
|
||||||
.trim(from: 0, to: progress)
|
|
||||||
.stroke(accent, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
|
||||||
.rotationEffect(.degrees(-90))
|
|
||||||
}
|
|
||||||
.frame(width: diameter, height: diameter)
|
|
||||||
.animation(.smooth(duration: 0.25), value: progress)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct MinimalisticReminderDetailsView: View {
|
private struct MinimalisticReminderDetailsView: View {
|
||||||
let entry: ReminderLiveActivityManager.ReminderEntry
|
let entry: ReminderLiveActivityManager.ReminderEntry
|
||||||
let linkURL: URL?
|
let linkURL: URL?
|
||||||
|
var onHoverChanged: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
private let detailFont = Font.system(size: 13, weight: .regular)
|
private let detailFont = Font.system(size: 13, weight: .regular)
|
||||||
private let smallLabelFont = Font.system(size: 12, weight: .semibold)
|
private let smallLabelFont = Font.system(size: 12, weight: .semibold)
|
||||||
|
|
@ -285,6 +325,12 @@ private struct MinimalisticReminderDetailsView: View {
|
||||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
.fill(Color.black.opacity(0.92))
|
.fill(Color.black.opacity(0.92))
|
||||||
)
|
)
|
||||||
|
.onHover { hovering in
|
||||||
|
onHoverChanged(hovering)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
onHoverChanged(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func detailRow(icon: String, label: String, value: String) -> some View {
|
private func detailRow(icon: String, label: String, value: String) -> some View {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
@Published var isClipboardPopoverActive: Bool = false
|
@Published var isClipboardPopoverActive: Bool = false
|
||||||
@Published var isColorPickerPopoverActive: Bool = false
|
@Published var isColorPickerPopoverActive: Bool = false
|
||||||
@Published var isStatsPopoverActive: Bool = false
|
@Published var isStatsPopoverActive: Bool = false
|
||||||
|
@Published var isReminderPopoverActive: Bool = false
|
||||||
@Published var isMediaOutputPopoverActive: Bool = false
|
@Published var isMediaOutputPopoverActive: Bool = false
|
||||||
@Published var isTimerPopoverActive: Bool = false
|
@Published var isTimerPopoverActive: Bool = false
|
||||||
@Published var shouldRecheckHover: Bool = false
|
@Published var shouldRecheckHover: Bool = false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue