diff --git a/Calendarr iOS.xcodeproj/project.pbxproj b/Calendarr iOS.xcodeproj/project.pbxproj index 85ec7a3..c74c66e 100644 --- a/Calendarr iOS.xcodeproj/project.pbxproj +++ b/Calendarr iOS.xcodeproj/project.pbxproj @@ -498,6 +498,7 @@ INFOPLIST_KEY_CFBundleDisplayName = Calendarr; INFOPLIST_KEY_CFBundleName = Calendarr; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -509,7 +510,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios; PRODUCT_NAME = "Calendarr iOS"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -540,6 +541,7 @@ INFOPLIST_KEY_CFBundleDisplayName = Calendarr; INFOPLIST_KEY_CFBundleName = Calendarr; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -551,7 +553,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2; + MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios; PRODUCT_NAME = "Calendarr iOS"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/Calendarr iOS/Models/CalendarStore.swift b/Calendarr iOS/Models/CalendarStore.swift index e567842..46b29fb 100644 --- a/Calendarr iOS/Models/CalendarStore.swift +++ b/Calendarr iOS/Models/CalendarStore.swift @@ -277,11 +277,13 @@ class CalendarStore { } /// Trigger a full cache reload (e.g. when cache-range setting changes). + /// Intentionally keeps `events` intact so the UI stays populated while + /// the network fetch runs; `refreshFromCache` will swap in fresh data + /// atomically once it arrives. func invalidateCache() { cachedStart = nil cachedEnd = nil allCachedEvents = [] - events = [] } private func mergeIntoCache(_ newEvents: [CalEvent], rangeStart: Date, rangeEnd: Date) { diff --git a/Calendarr iOS/Models/Localization.swift b/Calendarr iOS/Models/Localization.swift index 7f2b548..0e896ef 100644 --- a/Calendarr iOS/Models/Localization.swift +++ b/Calendarr iOS/Models/Localization.swift @@ -213,6 +213,8 @@ private let strings: [String: [String: String]] = [ "event.reset_color": "Zurücksetzen", "event.edit_title": "Termin bearbeiten", "event.new_title": "Neuer Termin", + "event.copy_title": "Termin kopieren", + "event.copy_to": "In Kalender kopieren", "event.save": "Sichern", "event.add": "Hinzufügen", @@ -472,6 +474,8 @@ private let strings: [String: [String: String]] = [ "event.reset_color": "Reset", "event.edit_title": "Edit event", "event.new_title": "New event", + "event.copy_title": "Copy event", + "event.copy_to": "Copy to calendar", "event.save": "Save", "event.add": "Add", diff --git a/Calendarr iOS/Views/Calendar/CalendarHostView.swift b/Calendarr iOS/Views/Calendar/CalendarHostView.swift index 15ec78f..19aaf48 100644 --- a/Calendarr iOS/Views/Calendar/CalendarHostView.swift +++ b/Calendarr iOS/Views/Calendar/CalendarHostView.swift @@ -1,5 +1,16 @@ import SwiftUI +private enum CalEditorContext: Identifiable { + case create(Date) + case edit(CalEvent) + var id: String { + switch self { + case .create(let d): return "new-\(d.timeIntervalSince1970)" + case .edit(let ev): return "edit-\(ev.id)" + } + } +} + struct CalendarHostView: View { let api: CalendarrAPI @Binding var showMenu: Bool @@ -14,9 +25,7 @@ struct CalendarHostView: View { @Environment(\.scenePhase) private var scenePhase @State private var store = CalendarStore() - @State private var showEditor = false - @State private var editorDate: Date = .now - @State private var editingEvent: CalEvent? = nil + @State private var editorContext: CalEditorContext? = nil @State private var selectedEvent: CalEvent? = nil @State private var visibleMonth: Date = .now @State private var showFilter = false @@ -40,6 +49,19 @@ struct CalendarHostView: View { } } + // MARK: – Loading indicator + + @ViewBuilder + private var loadingIndicator: some View { + if store.isLoading || store.isCachingBackground { + ProgressView() + .padding(14) + .background(.regularMaterial, in: Circle()) + .shadow(color: .black.opacity(0.12), radius: 6, y: 2) + .transition(.opacity.combined(with: .scale(scale: 0.85))) + } + } + // MARK: – Flat variant private var flatVariant: some View { @@ -51,22 +73,11 @@ struct CalendarHostView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(hex: bgHex)) .overlay(alignment: .top) { - if store.isLoading { - ProgressView().padding(.top, 10).transition(.opacity) - } + loadingIndicator.padding(.top, 12) } + .animation(.easeInOut(duration: 0.2), value: store.isLoading || store.isCachingBackground) } .overlay(alignment: .bottomTrailing) { solidFAB } - // Subtle background cache indicator (top-leading) - .overlay(alignment: .topLeading) { - if store.isCachingBackground { - Image(systemName: "arrow.triangle.2.circlepath") - .font(.caption2) - .foregroundStyle(.secondary) - .padding(6) - .transition(.opacity) - } - } .modifier(calendarSheets) .task { await startup() } .onChange(of: store.currentDate) { _, _ in Task { await onNavigate() } } @@ -93,10 +104,9 @@ struct CalendarHostView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(hex: bgHex)) .overlay(alignment: .top) { - if store.isLoading { - ProgressView().padding(.top, 10).transition(.opacity) - } + loadingIndicator.padding(.top, 12) } + .animation(.easeInOut(duration: 0.2), value: store.isLoading || store.isCachingBackground) .overlay(alignment: .top) { if let err = store.lastError { errorBannerView(err).padding(.top, 8) } } @@ -247,13 +257,9 @@ struct CalendarHostView: View { case .month: // Month view uses vertical scroll – no horizontal swipe. MonthView(store: store, - onDayTap: { editorDate = $0 }, + onDayTap: { store.currentDate = $0 }, onEventTap: { selectedEvent = $0 }, - onCreateEvent: { day in - editingEvent = nil - editorDate = day - showEditor = true - }, + onCreateEvent: { day in editorContext = .create(day) }, onShowWeek: { day in store.currentDate = day store.viewType = .week @@ -266,11 +272,7 @@ struct CalendarHostView: View { case .week: WeekView(store: store, onEventTap: { selectedEvent = $0 }, - onCreateEvent: { date in - editingEvent = nil - editorDate = date - showEditor = true - }, + onCreateEvent: { date in editorContext = .create(date) }, onShowMonth: { date in store.currentDate = date store.viewType = .month @@ -283,11 +285,7 @@ struct CalendarHostView: View { case .day: DayView(store: store, onEventTap: { selectedEvent = $0 }, - onCreateEvent: { date in - editingEvent = nil - editorDate = date - showEditor = true - }) + onCreateEvent: { date in editorContext = .create(date) }) .simultaneousGesture(swipe) case .quarter: QuarterView(store: store, onEventTap: { selectedEvent = $0 }) @@ -302,7 +300,7 @@ struct CalendarHostView: View { /// Standard solid FAB (flat mode) private var solidFAB: some View { Button { - editingEvent = nil; editorDate = .now; showEditor = true + editorContext = .create(.now) } label: { Image(systemName: "plus") .font(.system(size: 22, weight: .semibold)) @@ -320,7 +318,7 @@ struct CalendarHostView: View { private var glassFAB: some View { if #available(iOS 26, *) { Button { - editingEvent = nil; editorDate = .now; showEditor = true + editorContext = .create(.now) } label: { Image(systemName: "plus") .font(.system(size: 22, weight: .semibold)) @@ -338,8 +336,7 @@ struct CalendarHostView: View { // MARK: – Sheets modifier private var calendarSheets: CalendarSheets { - CalendarSheets(store: store, showEditor: $showEditor, - editorDate: $editorDate, editingEvent: $editingEvent, + CalendarSheets(store: store, editorContext: $editorContext, selectedEvent: $selectedEvent, showFilter: $showFilter, api: api, reload: { await onNavigate() }, @@ -437,9 +434,7 @@ struct CalendarHostView: View { private struct CalendarSheets: ViewModifier { let store: CalendarStore - @Binding var showEditor: Bool - @Binding var editorDate: Date - @Binding var editingEvent: CalEvent? + @Binding var editorContext: CalEditorContext? @Binding var selectedEvent: CalEvent? @Binding var showFilter: Bool let api: CalendarrAPI @@ -448,21 +443,23 @@ private struct CalendarSheets: ViewModifier { func body(content: Content) -> some View { content - .sheet(isPresented: $showEditor) { + // Use sheet(item:) so the editing event is captured atomically – + // avoiding the race where sheet(isPresented:) evaluates its content + // before the editingEvent state update propagates. + .sheet(item: $editorContext) { ctx in + let editingEv: CalEvent? = { if case .edit(let ev) = ctx { return ev }; return nil }() + let date: Date = { if case .create(let d) = ctx { return d }; return .now }() EventEditorSheet(api: api, store: store, - initialDate: editorDate, editingEvent: editingEvent) { - // Create/edit changed server state → bust the cache so the - // new/updated event appears without a manual sync. - editingEvent = nil; await reloadForce() + initialDate: date, editingEvent: editingEv) { + editorContext = nil + await reloadForce() } } .sheet(item: $selectedEvent) { ev in - EventDetailSheet(event: ev, api: api, store: store) { updated in + EventDetailSheet(event: ev, api: api, store: store) { updated, needsForce in selectedEvent = nil - if let u = updated { editingEvent = u; showEditor = true } - // Delete already removed the event from the cache optimistically; - // a light cache refresh is enough here. - await reload() + if let u = updated { editorContext = .edit(u) } + if needsForce { await reloadForce() } else { await reload() } } } .sheet(isPresented: $showFilter) { diff --git a/Calendarr iOS/Views/Calendar/EventDetailSheet.swift b/Calendarr iOS/Views/Calendar/EventDetailSheet.swift index a1e7aa1..eaad07c 100644 --- a/Calendarr iOS/Views/Calendar/EventDetailSheet.swift +++ b/Calendarr iOS/Views/Calendar/EventDetailSheet.swift @@ -42,14 +42,10 @@ struct EventDetailSheet: View { } private var canEdit: Bool { - event.source == "local" || event.source == "caldav" + event.source == "local" || event.source == "caldav" || event.source == "homeassistant" } - /// Home Assistant events can't be edited in-app (no editor support), but - /// the server does support deleting them. - private var canDelete: Bool { - canEdit || event.source == "homeassistant" - } + private var canDelete: Bool { canEdit } var body: some View { NavigationStack { diff --git a/Calendarr iOS/Views/Calendar/EventEditorSheet.swift b/Calendarr iOS/Views/Calendar/EventEditorSheet.swift index a967b45..d411325 100644 --- a/Calendarr iOS/Views/Calendar/EventEditorSheet.swift +++ b/Calendarr iOS/Views/Calendar/EventEditorSheet.swift @@ -5,7 +5,7 @@ struct EventEditorSheet: View { let store: CalendarStore let initialDate: Date let editingEvent: CalEvent? - let copyFrom: CalEvent? + var copyFrom: CalEvent? = nil let onSaved: () async -> Void @Environment(\.dismiss) var dismiss @@ -133,16 +133,27 @@ struct EventEditorSheet: View { title = ev.title isAllDay = ev.isAllDay startDate = ev.startDate - endDate = ev.endDate + // All-day end dates are stored as exclusive (day after last); subtract 1 for the picker. + endDate = ev.isAllDay + ? Calendar.current.date(byAdding: .day, value: -1, to: ev.endDate) ?? ev.endDate + : ev.endDate location = ev.location notes = ev.notes color = ev.color ?? "" - selectedCalendarId = ev.calendarId + // HA events use "homeassistant-42" in CalEvent but "ha-42" in WritableCalendar + if ev.source == "homeassistant" { + let num = ev.calendarId.replacingOccurrences(of: "homeassistant-", with: "") + selectedCalendarId = "ha-\(num)" + } else { + selectedCalendarId = ev.calendarId + } } else if let ev = copyFrom { title = ev.title isAllDay = ev.isAllDay startDate = ev.startDate - endDate = ev.endDate + endDate = ev.isAllDay + ? Calendar.current.date(byAdding: .day, value: -1, to: ev.endDate) ?? ev.endDate + : ev.endDate location = ev.location notes = ev.notes color = ev.color ?? "" @@ -168,10 +179,19 @@ struct EventEditorSheet: View { do { if let ev = editingEvent { - if ev.source == "local" { + switch ev.source { + case "local": try await api.updateLocalEvent(uid: ev.id, title: title, start: start, end: end, isAllDay: isAllDay, location: location, description: notes, color: colorVal) - } else { + case "homeassistant": + // No update API exists – delete the old event and recreate with new data. + let rawId = ev.calendarId.replacingOccurrences(of: "homeassistant-", with: "") + let haCalId = Int(rawId) ?? 0 + try await api.deleteHAEvent(calendarId: haCalId, uid: ev.id) + try await api.createHAEvent(calendarId: haCalId, title: title, + start: start, end: end, isAllDay: isAllDay, + location: location, description: notes) + default: // caldav let calId = Int(ev.calendarId) try await api.updateCalDAVEvent(uid: ev.id, url: ev.url, calendarId: calId, title: title, start: start, end: end, isAllDay: isAllDay, diff --git a/Calendarr iOS/Views/Calendar/MonthView.swift b/Calendarr iOS/Views/Calendar/MonthView.swift index aeb24b2..1a1f77b 100644 --- a/Calendarr iOS/Views/Calendar/MonthView.swift +++ b/Calendarr iOS/Views/Calendar/MonthView.swift @@ -53,8 +53,7 @@ struct MonthView: View { headerRow Divider() ScrollView { - LazyVStack(spacing: 0) { - ForEach(weekStarts, id: \.self) { ws in + LazyVStack(spacing: 0) { ForEach(weekStarts, id: \.self) { ws in WeekRow(weekStart: ws, store: store, dividerColor: Color(hex: dividerHex), @@ -72,6 +71,7 @@ struct MonthView: View { } .scrollTargetLayout() } + .scrollIndicators(.hidden) .scrollPosition(id: $scrolledWeek, anchor: .top) .onAppear { if !didInitialScroll { diff --git a/CalendarrWidgets/CalendarrWidgets.swift b/CalendarrWidgets/CalendarrWidgets.swift index 67e59bb..8cb6cb9 100644 --- a/CalendarrWidgets/CalendarrWidgets.swift +++ b/CalendarrWidgets/CalendarrWidgets.swift @@ -12,7 +12,11 @@ struct CalendarrWidgetBundle: WidgetBundle { UpcomingWidget() UpNextWidget() CalendarDayWidget() + TwoMonthWidget() + NowNextEventsWidget() LockScreenWidget() + LockScreenCountWidget() + LockScreenCountdownWidget() } } @@ -157,7 +161,37 @@ struct CalendarDayWidget: Widget { } } -// MARK: – Lock Screen (circular, rectangular, inline) +// MARK: – Two Month calendar grid (medium + large) + +struct TwoMonthWidget: Widget { + let kind: String = "TwoMonthWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in + TwoMonthWidgetView(entry: entry).calendarrChrome(entry.snapshot) + } + .configurationDisplayName(WidgetL10n.t("widget.display.twomonth_title", "system")) + .description(WidgetL10n.t("widget.display.twomonth_desc", "system")) + .supportedFamilies([.systemMedium, .systemLarge]) + } +} + +// MARK: – Now & Next events (medium) + +struct NowNextEventsWidget: Widget { + let kind: String = "NowNextEventsWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in + NowNextWidgetView(entry: entry).calendarrChrome(entry.snapshot) + } + .configurationDisplayName(WidgetL10n.t("widget.display.nownext_title", "system")) + .description(WidgetL10n.t("widget.display.nownext_desc", "system")) + .supportedFamilies([.systemMedium]) + } +} + +// MARK: – Lock Screen: date (circular, rectangular, inline) struct LockScreenWidget: Widget { let kind: String = "LockScreenWidget" @@ -173,3 +207,37 @@ struct LockScreenWidget: Widget { .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline]) } } + +// MARK: – Lock Screen: today event count (circular, rectangular, inline) + +struct LockScreenCountWidget: Widget { + let kind: String = "LockScreenCountWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in + LockScreenCountWidgetView(entry: entry) + .containerBackground(for: .widget) { Color.clear } + .environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system")) + } + .configurationDisplayName(WidgetL10n.t("widget.display.lockscreen_count_title", "system")) + .description(WidgetL10n.t("widget.display.lockscreen_count_desc", "system")) + .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline]) + } +} + +// MARK: – Lock Screen: countdown to next event (circular, rectangular, inline) + +struct LockScreenCountdownWidget: Widget { + let kind: String = "LockScreenCountdownWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in + LockScreenCountdownWidgetView(entry: entry) + .containerBackground(for: .widget) { Color.clear } + .environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system")) + } + .configurationDisplayName(WidgetL10n.t("widget.display.lockscreen_countdown_title", "system")) + .description(WidgetL10n.t("widget.display.lockscreen_countdown_desc", "system")) + .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline]) + } +} diff --git a/CalendarrWidgets/LockScreenWidgetViews.swift b/CalendarrWidgets/LockScreenWidgetViews.swift index 63b3771..dc03981 100644 --- a/CalendarrWidgets/LockScreenWidgetViews.swift +++ b/CalendarrWidgets/LockScreenWidgetViews.swift @@ -1,6 +1,8 @@ import SwiftUI import WidgetKit +// MARK: – Date widget (existing) + struct LockScreenWidgetView: View { let entry: CalendarrEntry @Environment(\.widgetFamily) private var family @@ -97,3 +99,190 @@ struct LockScreenWidgetView: View { return Label(text, systemImage: "calendar") } } + +// MARK: – Today event count widget + +struct LockScreenCountWidgetView: View { + let entry: CalendarrEntry + @Environment(\.widgetFamily) private var family + + private var snapshot: WidgetSnapshot? { entry.snapshot } + private var lang: String { snapshot?.language ?? "system" } + + private var timeFmt: DateFormatter { + let f = DateFormatter() + f.locale = WidgetL10n.locale(lang) + f.dateFormat = "HH:mm" + return f + } + + private var todayEvents: [WidgetEvent] { + guard let s = snapshot else { return [] } + return WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s) + } + + @ViewBuilder + var body: some View { + switch family { + case .accessoryCircular: + circularView + case .accessoryRectangular: + rectangularView + default: + inlineView + } + } + + private var circularView: some View { + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 1) { + Image(systemName: "calendar") + .font(.system(size: 10, weight: .semibold)) + .widgetAccentable() + Text("\(todayEvents.count)") + .font(.system(size: 22, weight: .bold)) + .minimumScaleFactor(0.7) + .widgetAccentable() + } + } + } + + private var rectangularView: some View { + let countLabel = "\(todayEvents.count) \(WidgetL10n.t("widget.events_count", lang))" + return VStack(alignment: .leading, spacing: 3) { + HStack(spacing: 4) { + Text(WidgetL10n.t("widget.today", lang).uppercased()) + .font(.system(size: 9, weight: .bold)) + .widgetAccentable() + Text("· \(countLabel)") + .font(.system(size: 9)) + } + if todayEvents.isEmpty { + Text(WidgetL10n.t("widget.no_events", lang)) + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } else { + ForEach(todayEvents.prefix(2)) { ev in + HStack(spacing: 4) { + Text(ev.isAllDay ? "·" : timeFmt.string(from: ev.start)) + .font(.system(size: 10, weight: .semibold)) + .widgetAccentable() + .frame(width: 32, alignment: .leading) + Text(ev.title) + .font(.system(size: 11, weight: .medium)) + .lineLimit(1) + } + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private var inlineView: some View { + let label = "\(todayEvents.count) \(WidgetL10n.t("widget.events_count", lang))" + return Label(label, systemImage: "calendar.badge.clock") + } +} + +// MARK: – Countdown to next event widget + +struct LockScreenCountdownWidgetView: View { + let entry: CalendarrEntry + @Environment(\.widgetFamily) private var family + + private var snapshot: WidgetSnapshot? { entry.snapshot } + private var lang: String { snapshot?.language ?? "system" } + + private var timeFmt: DateFormatter { + let f = DateFormatter() + f.locale = WidgetL10n.locale(lang) + f.dateFormat = "HH:mm" + return f + } + + private var nextEvent: WidgetEvent? { + guard let s = snapshot else { return nil } + return WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s).first + } + + private var isRunning: Bool { + guard let ev = nextEvent, !ev.isAllDay else { return false } + return ev.start <= entry.date && ev.end > entry.date + } + + private var countdownText: String { + guard let ev = nextEvent else { return WidgetL10n.t("widget.no_events", lang) } + if isRunning { return WidgetL10n.t("widget.running", lang) } + if ev.isAllDay { return WidgetL10n.t("widget.allday", lang) } + let total = Int(max(0, ev.start.timeIntervalSince(entry.date)) / 60) + if total < 60 { return "in \(total)m" } + let h = total / 60; let m = total % 60 + return m == 0 ? "in \(h)h" : "in \(h)h \(m)m" + } + + @ViewBuilder + var body: some View { + switch family { + case .accessoryCircular: + circularView + case .accessoryRectangular: + rectangularView + default: + inlineView + } + } + + private var circularView: some View { + ZStack { + AccessoryWidgetBackground() + VStack(spacing: 1) { + Text(countdownText) + .font(.system(size: 13, weight: .bold)) + .minimumScaleFactor(0.5) + .lineLimit(1) + .widgetAccentable() + if let ev = nextEvent, !ev.isAllDay { + Text(timeFmt.string(from: ev.start)) + .font(.system(size: 8)) + .lineLimit(1) + } + } + .padding(.horizontal, 4) + } + } + + private var rectangularView: some View { + VStack(alignment: .leading, spacing: 2) { + if let ev = nextEvent { + Text(countdownText) + .font(.system(size: 11, weight: .semibold)) + .widgetAccentable() + Text(ev.title) + .font(.system(size: 14, weight: .bold)) + .lineLimit(1) + let timeStr = ev.isAllDay + ? WidgetL10n.t("widget.allday", lang) + : "\(timeFmt.string(from: ev.start)) – \(timeFmt.string(from: ev.end))" + Text(timeStr) + .font(.system(size: 11)) + .lineLimit(1) + } else { + Image(systemName: "timer") + .font(.system(size: 13)) + .widgetAccentable() + Text(WidgetL10n.t("widget.no_events", lang)) + .font(.system(size: 13)) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private var inlineView: some View { + let text: String = { + guard let ev = nextEvent else { return WidgetL10n.t("widget.no_events", lang) } + return "\(ev.title) \(countdownText)" + }() + return Label(text, systemImage: "timer") + } +} diff --git a/CalendarrWidgets/NowNextWidgetView.swift b/CalendarrWidgets/NowNextWidgetView.swift index c8f724e..6a06400 100644 --- a/CalendarrWidgets/NowNextWidgetView.swift +++ b/CalendarrWidgets/NowNextWidgetView.swift @@ -62,7 +62,7 @@ struct NowNextWidgetView: View { private func featuredCard(snapshot: WidgetSnapshot) -> some View { let ev = featuredEvent - let baseColor = ev.map { Color(widgetHex: $0.colorHex) } ?? Color.accentColor.opacity(0.5) + let baseColor = ev.map { Color(widgetHex: $0.colorHex) } ?? Color(widgetHex: snapshot.primaryColorHex) return ZStack(alignment: .leading) { LinearGradient( diff --git a/CalendarrWidgets/WidgetSupport.swift b/CalendarrWidgets/WidgetSupport.swift index f080405..8df82c0 100644 --- a/CalendarrWidgets/WidgetSupport.swift +++ b/CalendarrWidgets/WidgetSupport.swift @@ -64,8 +64,19 @@ enum WidgetL10n { "widget.display.upnext_desc": "Nächste Termine mit Monatsübersicht.", "widget.display.calday_title": "Tag & Termine", "widget.display.calday_desc": "Datum, Wochenübersicht und nächste Termine.", - "widget.display.lockscreen_title": "Sperrbildschirm", - "widget.display.lockscreen_desc": "Datum und nächster Termin auf dem Sperrbildschirm." + "widget.display.lockscreen_title": "Datum", + "widget.display.lockscreen_desc": "Aktuelles Datum und nächster Termin.", + "widget.display.twomonth_title": "Zwei Monate", + "widget.display.twomonth_desc": "Aktueller und nächster Monat auf einen Blick.", + "widget.display.nownext_title": "Jetzt & Nächstes", + "widget.display.nownext_desc": "Aktueller Termin und nächste Ereignisse.", + "widget.cw": "KW", + "widget.running": "Läuft", + "widget.events_count": "Termine", + "widget.display.lockscreen_count_title": "Termine heute", + "widget.display.lockscreen_count_desc": "Anzahl und Liste heutiger Termine.", + "widget.display.lockscreen_countdown_title": "Countdown", + "widget.display.lockscreen_countdown_desc": "Zeit bis zum nächsten Termin." ], "en": [ "widget.today": "Today", @@ -91,8 +102,19 @@ enum WidgetL10n { "widget.display.upnext_desc": "Next events with month overview.", "widget.display.calday_title": "Day & Events", "widget.display.calday_desc": "Date, week overview and upcoming events.", - "widget.display.lockscreen_title": "Lock Screen", - "widget.display.lockscreen_desc": "Date and next event on the lock screen." + "widget.display.lockscreen_title": "Date", + "widget.display.lockscreen_desc": "Current date and next event.", + "widget.display.twomonth_title": "Two Months", + "widget.display.twomonth_desc": "Current and next month at a glance.", + "widget.display.nownext_title": "Now & Next", + "widget.display.nownext_desc": "Current event and upcoming events.", + "widget.cw": "W", + "widget.running": "Running", + "widget.events_count": "Events", + "widget.display.lockscreen_count_title": "Today's Events", + "widget.display.lockscreen_count_desc": "Count and list of today's events.", + "widget.display.lockscreen_countdown_title": "Countdown", + "widget.display.lockscreen_countdown_desc": "Time until your next event." ] ] }