feat: custom reminder picker, muted-calendar hint, synced default duration
- Reminder editor: presets + custom number+unit (minutes/hours/days/weeks) - Grey out + footer hint when the selected calendar's reminders are muted; reminders are kept, scheduler already skips them - New synced setting defaultEventDurationMinutes for new events Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ struct AppSettings: Codable {
|
||||
var privateEventVisibility: String = "busy" // 'hidden' | 'busy'
|
||||
var groupVisibleCalendarId: Int? = nil
|
||||
var defaultReminderMinutes: Int? = nil // minutes before start; nil = off
|
||||
var defaultEventDurationMinutes: Int = 60 // applied to a new event's end time
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case defaultView = "default_view"
|
||||
@@ -39,6 +40,7 @@ struct AppSettings: Codable {
|
||||
case privateEventVisibility = "private_event_visibility"
|
||||
case groupVisibleCalendarId = "group_visible_calendar_id"
|
||||
case defaultReminderMinutes = "default_reminder_minutes"
|
||||
case defaultEventDurationMinutes = "default_event_duration_minutes"
|
||||
}
|
||||
|
||||
init() {}
|
||||
@@ -69,6 +71,7 @@ struct AppSettings: Codable {
|
||||
privateEventVisibility = try c.decodeIfPresent(String.self, forKey: .privateEventVisibility) ?? d.privateEventVisibility
|
||||
groupVisibleCalendarId = try c.decodeIfPresent(Int.self, forKey: .groupVisibleCalendarId)
|
||||
defaultReminderMinutes = try c.decodeIfPresent(Int.self, forKey: .defaultReminderMinutes)
|
||||
defaultEventDurationMinutes = try c.decodeIfPresent(Int.self, forKey: .defaultEventDurationMinutes) ?? d.defaultEventDurationMinutes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ private let strings: [String: [String: String]] = [
|
||||
"settings.monday": "Montag",
|
||||
"settings.sunday": "Sonntag",
|
||||
"settings.dimpast": "Vergangene Termine ausgrauen",
|
||||
"settings.default_duration": "Standard-Termindauer",
|
||||
"settings.nav.profile": "Profil",
|
||||
"settings.privacy": "Privatsphäre",
|
||||
"settings.private_visibility": "Private Termine für Gruppen",
|
||||
@@ -455,6 +456,7 @@ private let strings: [String: [String: String]] = [
|
||||
"settings.monday": "Monday",
|
||||
"settings.sunday": "Sunday",
|
||||
"settings.dimpast": "Dim past events",
|
||||
"settings.default_duration": "Default event duration",
|
||||
"settings.nav.profile": "Profile",
|
||||
"settings.privacy": "Privacy",
|
||||
"settings.private_visibility": "Private events for groups",
|
||||
|
||||
@@ -7,6 +7,63 @@ enum ReminderOptions {
|
||||
/// Selectable offsets in minutes-before-start.
|
||||
static let all: [Int] = [0, 5, 15, 30, 60, 1440, 10080]
|
||||
|
||||
/// Quick presets shown in the picker; everything else is entered as a
|
||||
/// custom number + unit. Default for a freshly-switched custom row.
|
||||
static let presets: [Int] = [0, 30, 1440] // at start, 30 min, 1 day
|
||||
static let customDefault = 120 // 2 hours (deliberately not a preset)
|
||||
|
||||
/// Time units for the custom picker (value × mult = minutes-before-start).
|
||||
enum Unit: Int, CaseIterable, Identifiable {
|
||||
case minutes, hours, days, weeks
|
||||
var id: Int { rawValue }
|
||||
var mult: Int {
|
||||
switch self {
|
||||
case .minutes: return 1
|
||||
case .hours: return 60
|
||||
case .days: return 1440
|
||||
case .weeks: return 10080
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a minutes value into the largest exact {value, unit} for the custom picker.
|
||||
static func split(_ minutes: Int) -> (value: Int, unit: Unit) {
|
||||
for u in Unit.allCases.reversed() where minutes > 0 && minutes % u.mult == 0 {
|
||||
return (minutes / u.mult, u)
|
||||
}
|
||||
return (max(1, minutes), .minutes)
|
||||
}
|
||||
|
||||
static func unitLabel(_ u: Unit, _ l: String) -> String {
|
||||
let en = isEnglish(l)
|
||||
switch u {
|
||||
case .minutes: return en ? "minutes" : "Minuten"
|
||||
case .hours: return en ? "hours" : "Stunden"
|
||||
case .days: return en ? "days" : "Tage"
|
||||
case .weeks: return en ? "weeks" : "Wochen"
|
||||
}
|
||||
}
|
||||
/// Human label for a duration in minutes (no "before" suffix), e.g. "1 h", "30 min".
|
||||
static func durationLabel(_ minutes: Int, _ l: String) -> String {
|
||||
let en = isEnglish(l)
|
||||
if minutes % 60 == 0 {
|
||||
let h = minutes / 60
|
||||
return en ? "\(h) h" : "\(h) Std."
|
||||
}
|
||||
if minutes > 60 {
|
||||
let h = minutes / 60, m = minutes % 60
|
||||
return "\(h):\(String(format: "%02d", m)) h"
|
||||
}
|
||||
return en ? "\(minutes) min" : "\(minutes) Min."
|
||||
}
|
||||
static func customLabel(_ l: String) -> String { isEnglish(l) ? "Custom…" : "Benutzerdefiniert…" }
|
||||
static func beforeLabel(_ l: String) -> String { isEnglish(l) ? "before" : "vorher" }
|
||||
static func disabledNote(_ l: String) -> String {
|
||||
isEnglish(l)
|
||||
? "Reminders are disabled for this calendar – they will not fire."
|
||||
: "Für diesen Kalender sind Benachrichtigungen deaktiviert – Erinnerungen werden nicht ausgeführt."
|
||||
}
|
||||
|
||||
private static func isEnglish(_ appLang: String) -> Bool {
|
||||
if appLang == "en" { return true }
|
||||
if appLang == "de" { return false }
|
||||
|
||||
Reference in New Issue
Block a user