Temporary push: Will fix the animation later

This commit is contained in:
Hariharan Mudaliar 2025-11-11 22:23:31 +05:30
commit b7cf4dcf0c
3 changed files with 112 additions and 64 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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