mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-29 08:04:32 +00:00
Add alert indicator before reminder
This commit is contained in:
parent
12db48bc25
commit
929b15ba45
3 changed files with 66 additions and 41 deletions
|
|
@ -474,7 +474,7 @@ struct ContentView: View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
MarqueeText(
|
MarqueeText(
|
||||||
.constant(reminderSneakPeekText(for: reminder)),
|
.constant(reminderSneakPeekText(for: reminder)),
|
||||||
textColor: reminderColor(for: reminder.event),
|
textColor: reminderColor(for: reminder, now: reminderManager.currentDate),
|
||||||
minDuration: 1,
|
minDuration: 1,
|
||||||
frameWidth: geo.size.width
|
frameWidth: geo.size.width
|
||||||
)
|
)
|
||||||
|
|
@ -522,8 +522,13 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reminderColor(for event: EventModel) -> Color {
|
private func reminderColor(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> Color {
|
||||||
Color(nsColor: event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
let window = TimeInterval(Defaults[.reminderSneakPeekDuration])
|
||||||
|
let remaining = reminder.event.start.timeIntervalSince(now)
|
||||||
|
if window > 0 && remaining > 0 && remaining <= window {
|
||||||
|
return .red
|
||||||
|
}
|
||||||
|
return Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reminderSneakPeekText(for entry: ReminderLiveActivityManager.ReminderEntry) -> String {
|
private func reminderSneakPeekText(for entry: ReminderLiveActivityManager.ReminderEntry) -> String {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ struct ReminderLiveActivity: View {
|
||||||
Color.clear
|
Color.clear
|
||||||
.frame(width: leftWingWidth, height: notchContentHeight)
|
.frame(width: leftWingWidth, height: notchContentHeight)
|
||||||
.background(alignment: .leading) {
|
.background(alignment: .leading) {
|
||||||
iconSection(for: reminder)
|
iconSection(for: reminder, now: now)
|
||||||
.padding(.leading, wingPadding / 2)
|
.padding(.leading, wingPadding / 2)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
@ -62,11 +62,11 @@ struct ReminderLiveActivity: View {
|
||||||
.frame(height: notchContentHeight, alignment: .center)
|
.frame(height: notchContentHeight, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func iconSection(for reminder: ReminderLiveActivityManager.ReminderEntry) -> some View {
|
private func iconSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View {
|
||||||
let diameter = iconDiameter
|
let diameter = iconDiameter
|
||||||
let accent = accentColor(for: reminder)
|
let accent = accentColor(for: reminder, now: now)
|
||||||
|
|
||||||
return Image(systemName: glyphName(for: reminder.event))
|
return Image(systemName: iconName(for: reminder, now: now))
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.system(size: 16, weight: .semibold))
|
||||||
.foregroundStyle(accent)
|
.foregroundStyle(accent)
|
||||||
.frame(width: diameter, height: diameter)
|
.frame(width: diameter, height: diameter)
|
||||||
|
|
@ -75,49 +75,49 @@ struct ReminderLiveActivity: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func rightSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View {
|
private func rightSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View {
|
||||||
let accent = accentColor(for: reminder)
|
let accent = accentColor(for: reminder, now: now)
|
||||||
switch presentationStyle {
|
switch presentationStyle {
|
||||||
case .ringCountdown:
|
case .ringCountdown:
|
||||||
ringCountdownSection(for: reminder, now: now, accent: accent)
|
ringCountdownSection(for: reminder, now: now, accent: accent)
|
||||||
case .digital:
|
case .digital:
|
||||||
digitalSection(for: reminder, now: now)
|
digitalSection(for: reminder, now: now, accent: accent)
|
||||||
case .minutes:
|
case .minutes:
|
||||||
minutesSection(for: reminder, now: now)
|
minutesSection(for: reminder, now: now, accent: accent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ringCountdownSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View {
|
private func ringCountdownSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View {
|
||||||
let progress = progress(for: reminder, now: now)
|
let progressValue = progress(for: reminder, now: now)
|
||||||
let diameter = ringDiameter
|
let diameter = ringDiameter
|
||||||
|
|
||||||
return ZStack {
|
return ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.stroke(Color.white.opacity(0.15), lineWidth: ringStrokeWidth)
|
.stroke(Color.white.opacity(0.15), lineWidth: ringStrokeWidth)
|
||||||
Circle()
|
Circle()
|
||||||
.trim(from: 0, to: progress)
|
.trim(from: 0, to: progressValue)
|
||||||
.stroke(accent, style: StrokeStyle(lineWidth: ringStrokeWidth, lineCap: .round))
|
.stroke(accent, style: StrokeStyle(lineWidth: ringStrokeWidth, lineCap: .round))
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.animation(.smooth(duration: 0.25), value: progress)
|
.animation(.smooth(duration: 0.25), value: progressValue)
|
||||||
}
|
}
|
||||||
.frame(width: diameter, height: diameter)
|
.frame(width: diameter, height: diameter)
|
||||||
.frame(height: notchContentHeight, alignment: .center)
|
.frame(height: notchContentHeight, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func digitalSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View {
|
private func digitalSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View {
|
||||||
let countdown = digitalCountdown(for: reminder, now: now)
|
let countdown = digitalCountdown(for: reminder, now: now)
|
||||||
return Text(countdown)
|
return Text(countdown)
|
||||||
.font(.system(size: 16, weight: .semibold, design: .monospaced))
|
.font(.system(size: 16, weight: .semibold, design: .monospaced))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(accent)
|
||||||
.contentTransition(.numericText())
|
.contentTransition(.numericText())
|
||||||
.animation(.smooth(duration: 0.25), value: countdown)
|
.animation(.smooth(duration: 0.25), value: countdown)
|
||||||
.frame(height: notchContentHeight, alignment: .center)
|
.frame(height: notchContentHeight, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func minutesSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View {
|
private func minutesSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View {
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
Text(minutesCountdown(for: reminder, now: now))
|
Text(minutesCountdown(for: reminder, now: now))
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(accent)
|
||||||
}
|
}
|
||||||
.frame(height: notchContentHeight, alignment: .center)
|
.frame(height: notchContentHeight, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
@ -144,23 +144,15 @@ struct ReminderLiveActivity: View {
|
||||||
return minutes == 1 ? "in 1 min" : "in \(minutes) min"
|
return minutes == 1 ? "in 1 min" : "in \(minutes) min"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func glyphName(for event: EventModel) -> String {
|
private func accentColor(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> Color {
|
||||||
switch event.type {
|
if isCritical(for: reminder, now: now) {
|
||||||
case .birthday:
|
return .red
|
||||||
return "gift.fill"
|
|
||||||
case .reminder(let completed):
|
|
||||||
return completed ? "checkmark.circle" : "bell.fill"
|
|
||||||
case .event:
|
|
||||||
return event.isMeeting ? "person.2.fill" : "calendar"
|
|
||||||
}
|
}
|
||||||
}
|
return Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
||||||
|
|
||||||
private func accentColor(for reminder: ReminderLiveActivityManager.ReminderEntry) -> Color {
|
|
||||||
Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var leftWingWidth: CGFloat {
|
private var leftWingWidth: CGFloat {
|
||||||
wingPadding + iconDiameter
|
wingPadding + iconDiameter
|
||||||
}
|
}
|
||||||
|
|
||||||
private func rightWingWidth(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> CGFloat {
|
private func rightWingWidth(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> CGFloat {
|
||||||
|
|
@ -216,4 +208,15 @@ struct ReminderLiveActivity: View {
|
||||||
return UIFont.monospacedDigitSystemFont(ofSize: size, weight: weight)
|
return UIFont.monospacedDigitSystemFont(ofSize: size, weight: weight)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func iconName(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> String {
|
||||||
|
isCritical(for: reminder, now: now) ? ReminderLiveActivityManager.criticalIconName : ReminderLiveActivityManager.standardIconName
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isCritical(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> Bool {
|
||||||
|
let window = TimeInterval(Defaults[.reminderSneakPeekDuration])
|
||||||
|
guard window > 0 else { return false }
|
||||||
|
let remaining = reminder.event.start.timeIntervalSince(now)
|
||||||
|
return remaining > 0 && remaining <= window
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
static let shared = ReminderLiveActivityManager()
|
static let shared = ReminderLiveActivityManager()
|
||||||
|
static let standardIconName = "calendar.badge.clock"
|
||||||
|
static let criticalIconName = "calendar.badge.exclamationmark"
|
||||||
|
|
||||||
@Published private(set) var activeReminder: ReminderEntry?
|
@Published private(set) var activeReminder: ReminderEntry?
|
||||||
@Published private(set) var currentDate: Date = Date()
|
@Published private(set) var currentDate: Date = Date()
|
||||||
|
|
@ -36,6 +38,7 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
private let minimumRefreshInterval: TimeInterval = 10
|
private let minimumRefreshInterval: TimeInterval = 10
|
||||||
private let refreshDebounceInterval: TimeInterval = 0.3
|
private let refreshDebounceInterval: TimeInterval = 0.3
|
||||||
private var refreshTaskToken = UUID()
|
private var refreshTaskToken = UUID()
|
||||||
|
private var hasShownCriticalSneakPeek = false
|
||||||
|
|
||||||
private let calendarService: CalendarServiceProviding
|
private let calendarService: CalendarServiceProviding
|
||||||
private let calendarManager = CalendarManager.shared
|
private let calendarManager = CalendarManager.shared
|
||||||
|
|
@ -110,6 +113,7 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
pendingRefreshForce = false
|
pendingRefreshForce = false
|
||||||
refreshTask?.cancel()
|
refreshTask?.cancel()
|
||||||
refreshTask = nil
|
refreshTask = nil
|
||||||
|
hasShownCriticalSneakPeek = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deactivateReminder() {
|
private func deactivateReminder() {
|
||||||
|
|
@ -254,6 +258,7 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
private func handleEntrySelection(_ entry: ReminderEntry?, referenceDate: Date) {
|
private func handleEntrySelection(_ entry: ReminderEntry?, referenceDate: Date) {
|
||||||
fallbackRefreshTask?.cancel()
|
fallbackRefreshTask?.cancel()
|
||||||
nextReminder = entry
|
nextReminder = entry
|
||||||
|
hasShownCriticalSneakPeek = false
|
||||||
Task { await self.evaluateCurrentState(at: referenceDate) }
|
Task { await self.evaluateCurrentState(at: referenceDate) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,6 +322,7 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
activeReminder = nil
|
activeReminder = nil
|
||||||
nextReminder = nil
|
nextReminder = nil
|
||||||
stopTicker()
|
stopTicker()
|
||||||
|
hasShownCriticalSneakPeek = false
|
||||||
scheduleRefresh(force: true)
|
scheduleRefresh(force: true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -333,8 +339,28 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
type: .reminder,
|
type: .reminder,
|
||||||
duration: Defaults[.reminderSneakPeekDuration],
|
duration: Defaults[.reminderSneakPeekDuration],
|
||||||
value: 0,
|
value: 0,
|
||||||
icon: glyphName(for: entry.event)
|
icon: ReminderLiveActivityManager.standardIconName
|
||||||
)
|
)
|
||||||
|
hasShownCriticalSneakPeek = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let criticalWindow = TimeInterval(Defaults[.reminderSneakPeekDuration])
|
||||||
|
let timeRemaining = entry.event.start.timeIntervalSince(date)
|
||||||
|
if criticalWindow > 0 && timeRemaining > 0 {
|
||||||
|
if timeRemaining <= criticalWindow {
|
||||||
|
if !hasShownCriticalSneakPeek {
|
||||||
|
DynamicIslandViewCoordinator.shared.toggleSneakPeek(
|
||||||
|
status: true,
|
||||||
|
type: .reminder,
|
||||||
|
duration: Defaults[.reminderSneakPeekDuration],
|
||||||
|
value: 0,
|
||||||
|
icon: ReminderLiveActivityManager.criticalIconName
|
||||||
|
)
|
||||||
|
hasShownCriticalSneakPeek = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasShownCriticalSneakPeek = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startTickerIfNeeded()
|
startTickerIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -342,6 +368,7 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
activeReminder = nil
|
activeReminder = nil
|
||||||
}
|
}
|
||||||
stopTicker()
|
stopTicker()
|
||||||
|
hasShownCriticalSneakPeek = false
|
||||||
scheduleEvaluation(at: entry.triggerDate)
|
scheduleEvaluation(at: entry.triggerDate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -355,14 +382,4 @@ final class ReminderLiveActivityManager: ObservableObject {
|
||||||
await evaluateCurrentState(at: now)
|
await evaluateCurrentState(at: now)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func glyphName(for event: EventModel) -> String {
|
|
||||||
switch event.type {
|
|
||||||
case .birthday:
|
|
||||||
return "gift.fill"
|
|
||||||
case .reminder(let completed):
|
|
||||||
return completed ? "checkmark.circle" : "bell.fill"
|
|
||||||
case .event:
|
|
||||||
return event.isMeeting ? "person.2.fill" : "calendar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue