From a48223ec673acecaf87e4a36ded110a2bcb274b6 Mon Sep 17 00:00:00 2001 From: Cadmoonx Date: Sun, 16 Aug 2026 16:58:52 +1000 Subject: [PATCH 1/2] Add configurable calendar look-ahead window (CalendarDaysAhead) --- .../Helpers/CalendarExtensionMethods.cs | 20 ++++++++++++------- .../Interfaces/IServerBehaviorSettings.cs | 1 + .../Services/IcalCalendarService.cs | 9 ++++++--- .../Helpers/Config/ServerSettingsV1.cs | 2 ++ ImmichFrame.WebApi/Models/ServerSettings.cs | 1 + .../components/elements/appointments.svelte | 19 +++++++----------- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs b/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs index 19066902..3308e994 100644 --- a/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs +++ b/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs @@ -1,26 +1,32 @@ -using Ical.Net.CalendarComponents; +using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using ImmichFrame.Core.Interfaces; using ImmichFrame.Core.Models; - namespace ImmichFrame.WebApi.Helpers { public static class CalendarExtensionMethods { public static IAppointment ToAppointment(this Occurrence occurrence) { - if (occurrence.Source.GetType() == typeof(CalendarEvent)) { - return ((CalendarEvent)occurrence.Source).ToAppointment(); + string summary = ""; + string? description = null; + string? location = null; + + if (occurrence.Source is CalendarEvent calEvent) + { + summary = calEvent.Summary; + description = calEvent.Description; + location = calEvent.Location; } return new Appointment { - //Summary = occurrence.Period.Duration.Summary, - //Description = occurrence.Source.Description, + Summary = summary, + Description = description, StartTime = occurrence.Period.StartTime.AsSystemLocal, Duration = occurrence.Period.Duration, EndTime = occurrence.Period.EndTime.AsSystemLocal, - Location = "" + Location = location }; } public static IAppointment ToAppointment(this CalendarEvent calEvent) diff --git a/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs b/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs index 100abf39..d5f3f14e 100644 --- a/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs +++ b/ImmichFrame.Core/Interfaces/IServerBehaviorSettings.cs @@ -3,6 +3,7 @@ namespace ImmichFrame.Core.Interfaces public interface IServerBehaviorSettings { public List Webcalendars { get; } + public int CalendarDaysAhead { get; } public int RefreshAlbumPeopleInterval { get; } public string? WeatherApiKey { get; } public string? WeatherLatLong { get; } diff --git a/ImmichFrame.Core/Services/IcalCalendarService.cs b/ImmichFrame.Core/Services/IcalCalendarService.cs index 56ef89d1..738c4c1c 100644 --- a/ImmichFrame.Core/Services/IcalCalendarService.cs +++ b/ImmichFrame.Core/Services/IcalCalendarService.cs @@ -47,11 +47,14 @@ public async Task> GetAppointments() var icals = await GetCalendars(cals); + var endDate = DateTime.Today.AddDays(_serverSettings.CalendarDaysAhead + 1); + foreach (var ical in icals) { var calendar = Calendar.Load(ical); - - appointments.AddRange(calendar.GetOccurrences(DateTime.Today, DateTime.Today.AddDays(1)).Select(x => x.ToAppointment())); + appointments.AddRange(calendar.GetOccurrences(DateTime.Today, endDate) + .OrderBy(x => x.Period.StartTime.AsSystemLocal) + .Select(x => x.ToAppointment())); } return appointments; @@ -90,4 +93,4 @@ public async Task> GetCalendars(IEnumerable<(string? auth, string u return icals; } -} \ No newline at end of file +} diff --git a/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs b/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs index 076f36da..fed09782 100644 --- a/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs +++ b/ImmichFrame.WebApi/Helpers/Config/ServerSettingsV1.cs @@ -24,6 +24,7 @@ public class ServerSettingsV1 : IConfigSettable public List Tags { get; set; } = new List(); public int? Rating { get; set; } public List Webcalendars { get; set; } = new List(); + public int CalendarDaysAhead { get; set; } = 0; public int RefreshAlbumPeopleInterval { get; set; } = 12; public string? WeatherApiKey { get; set; } = string.Empty; public string? UnitSystem { get; set; } = "imperial"; @@ -101,6 +102,7 @@ public void ValidateAndInitialize() { } class GeneralSettingsV1Adapter(ServerSettingsV1 _delegate) : IGeneralSettings { public List Webcalendars => _delegate.Webcalendars; + public int CalendarDaysAhead => _delegate.CalendarDaysAhead; public int RefreshAlbumPeopleInterval => _delegate.RefreshAlbumPeopleInterval; public string? WeatherApiKey => _delegate.WeatherApiKey; public string? WeatherLatLong => _delegate.WeatherLatLong; diff --git a/ImmichFrame.WebApi/Models/ServerSettings.cs b/ImmichFrame.WebApi/Models/ServerSettings.cs index 74d0fb8e..3bbdbc8c 100644 --- a/ImmichFrame.WebApi/Models/ServerSettings.cs +++ b/ImmichFrame.WebApi/Models/ServerSettings.cs @@ -66,6 +66,7 @@ public class GeneralSettings : IGeneralSettings, IConfigSettable public string Layout { get; set; } = "splitview"; public int RenewImagesDuration { get; set; } = 30; public List Webcalendars { get; set; } = new(); + public int CalendarDaysAhead { get; set; } = 0; public int RefreshAlbumPeopleInterval { get; set; } = 12; public string? WeatherApiKey { get; set; } = string.Empty; public string? UnitSystem { get; set; } = "imperial"; diff --git a/immichFrame.Web/src/lib/components/elements/appointments.svelte b/immichFrame.Web/src/lib/components/elements/appointments.svelte index 645402a5..9fa38b95 100644 --- a/immichFrame.Web/src/lib/components/elements/appointments.svelte +++ b/immichFrame.Web/src/lib/components/elements/appointments.svelte @@ -4,43 +4,38 @@ import { format } from 'date-fns'; import { configStore } from '$lib/stores/config.store'; import { clientIdentifierStore } from '$lib/stores/persist.store'; - api.init(); - function formatDates(startTime: string, endTime: string) { let startDate = new Date(startTime); let endDate = new Date(endTime); - let sameDay = startDate.getDate() == endDate.getDate(); - + let sameDay = startDate.toDateString() == endDate.toDateString(); + let today = new Date(); + let isToday = startDate.toDateString() == today.toDateString(); let clockFormat = $configStore.clockFormat ?? 'HH:mm'; let clockDateFormat = $configStore.clockDateFormat ?? 'eee, MMM d'; let fullFormat = clockDateFormat + ' ' + clockFormat; - if (sameDay) { - return format(startDate, clockFormat) + ' - ' + format(endDate, clockFormat); + if (isToday) { + return format(startDate, clockFormat) + ' - ' + format(endDate, clockFormat); + } + return format(startDate, clockDateFormat) + ' ' + format(startDate, clockFormat) + ' - ' + format(endDate, clockFormat); } - return format(startDate, fullFormat) + ' - ' + format(endDate, fullFormat); } - let appointments: api.IAppointment[] = $state() as api.IAppointment[]; - onMount(() => { GetAppointments(); const appointmentInterval = setInterval(() => GetAppointments(), 10 * 60 * 1000); //every 10 minutes - return () => { clearInterval(appointmentInterval); }; }); - async function GetAppointments() { let appointmentRequest = await api.getAppointments({ clientIdentifier: $clientIdentifierStore }); if (appointmentRequest.status == 200) { appointments = appointmentRequest.data; - appointments = appointmentRequest.data.sort((a, b) => { return new Date(a.startTime ?? '').getTime() - new Date(b.startTime ?? '').getTime(); }); From 8698173b75021cc5bb4d2f35d06ef6d012a88e6e Mon Sep 17 00:00:00 2001 From: Cadmoonx Date: Sun, 16 Aug 2026 17:29:25 +1000 Subject: [PATCH 2/2] Address review feedback: fix nullability, validate CalendarDaysAhead, fix cross-calendar sort order --- ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs | 12 ++++++------ ImmichFrame.Core/Services/IcalCalendarService.cs | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs b/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs index 3308e994..c6f5e6e6 100644 --- a/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs +++ b/ImmichFrame.Core/Helpers/CalendarExtensionMethods.cs @@ -9,14 +9,14 @@ public static class CalendarExtensionMethods public static IAppointment ToAppointment(this Occurrence occurrence) { string summary = ""; - string? description = null; - string? location = null; + string description = ""; + string location = ""; if (occurrence.Source is CalendarEvent calEvent) { summary = calEvent.Summary; - description = calEvent.Description; - location = calEvent.Location; + description = calEvent.Description ?? ""; + location = calEvent.Location ?? ""; } return new Appointment @@ -34,11 +34,11 @@ public static IAppointment ToAppointment(this CalendarEvent calEvent) return new Appointment { Summary = calEvent.Summary, - Description = calEvent.Description, + Description = calEvent.Description ?? "", StartTime = calEvent.Start.AsSystemLocal, Duration = calEvent.Duration, EndTime = calEvent.End.AsSystemLocal, - Location = calEvent.Location + Location = calEvent.Location ?? "" }; } } diff --git a/ImmichFrame.Core/Services/IcalCalendarService.cs b/ImmichFrame.Core/Services/IcalCalendarService.cs index 738c4c1c..eb962ee5 100644 --- a/ImmichFrame.Core/Services/IcalCalendarService.cs +++ b/ImmichFrame.Core/Services/IcalCalendarService.cs @@ -47,17 +47,18 @@ public async Task> GetAppointments() var icals = await GetCalendars(cals); - var endDate = DateTime.Today.AddDays(_serverSettings.CalendarDaysAhead + 1); + // Clamp to a sane range: never negative, and cap well below DateTime overflow. + var daysAhead = Math.Clamp(_serverSettings.CalendarDaysAhead, 0, 3650); + var endDate = DateTime.Today.AddDays(daysAhead + 1); foreach (var ical in icals) { var calendar = Calendar.Load(ical); appointments.AddRange(calendar.GetOccurrences(DateTime.Today, endDate) - .OrderBy(x => x.Period.StartTime.AsSystemLocal) .Select(x => x.ToAppointment())); } - return appointments; + return appointments.OrderBy(x => x.StartTime).ToList(); }); }