From d1004a91116920808ef7ae48836e1cf9fb98b552 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 25 May 2026 10:33:31 +0200 Subject: [PATCH] Fix month scroll event disappearance by improving visible month detection and cache refresh rules --- Calendarr iOS.xcodeproj/project.pbxproj | 218 ++++++++++++++++-- .../Views/Calendar/CalendarHostView.swift | 43 +++- Calendarr iOS/Views/Calendar/MonthView.swift | 27 ++- 3 files changed, 263 insertions(+), 25 deletions(-) diff --git a/Calendarr iOS.xcodeproj/project.pbxproj b/Calendarr iOS.xcodeproj/project.pbxproj index 35473f6..778f47f 100644 --- a/Calendarr iOS.xcodeproj/project.pbxproj +++ b/Calendarr iOS.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 3927C7C02FB99D0F00EAD8ED /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -14,13 +18,45 @@ remoteGlobalIDString = C0000A01FB4E10100AB5001; remoteInfo = "Calendarr iOS"; }; + D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C0000301FB4E10100AB5001 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0001A01ABCDEF0100AB5001; + remoteInfo = CalendarrWidgets; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + D0001A0BABCDEF0100AB5001 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Calendarr iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C0000B01FC4E10100AB5001 /* Calendarr iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Calendarr iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CalendarrWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D0001A0FABCDEF0100AB5001 /* Exceptions for "CalendarrWidgets" folder in "CalendarrWidgets" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 3927C7BD2FB99D0F00EAD8ED /* Calendarr iOSTests */ = { isa = PBXFileSystemSynchronizedRootGroup; @@ -32,6 +68,19 @@ path = "Calendarr iOS"; sourceTree = ""; }; + D0001A03ABCDEF0100AB5001 /* CalendarrWidgets */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D0001A0FABCDEF0100AB5001 /* Exceptions for "CalendarrWidgets" folder in "CalendarrWidgets" target */, + ); + path = CalendarrWidgets; + sourceTree = ""; + }; + D0001A04ABCDEF0100AB5001 /* Shared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Shared; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +98,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D0001A06ABCDEF0100AB5001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -56,6 +112,8 @@ isa = PBXGroup; children = ( C0000D01FC4E10100AB5001 /* Calendarr iOS */, + D0001A03ABCDEF0100AB5001 /* CalendarrWidgets */, + D0001A04ABCDEF0100AB5001 /* Shared */, 3927C7BD2FB99D0F00EAD8ED /* Calendarr iOSTests */, C0000C01FB4E10100AB5001 /* Products */, ); @@ -66,6 +124,7 @@ children = ( C0000B01FC4E10100AB5001 /* Calendarr iOS.app */, 3927C7BC2FB99D0E00EAD8ED /* Calendarr iOSTests.xctest */, + D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */, ); name = Products; sourceTree = ""; @@ -103,13 +162,16 @@ C0000701FB4E10100AB5001 /* Sources */, C0000801FB4E10100AB5001 /* Frameworks */, C0000901FB4E10100AB5001 /* Resources */, + D0001A0BABCDEF0100AB5001 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( C0000D01FC4E10100AB5001 /* Calendarr iOS */, + D0001A04ABCDEF0100AB5001 /* Shared */, ); name = "Calendarr iOS"; packageProductDependencies = ( @@ -118,6 +180,29 @@ productReference = C0000B01FC4E10100AB5001 /* Calendarr iOS.app */; productType = "com.apple.product-type.application"; }; + D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0001A08ABCDEF0100AB5001 /* Build configuration list for PBXNativeTarget "CalendarrWidgets" */; + buildPhases = ( + D0001A05ABCDEF0100AB5001 /* Sources */, + D0001A06ABCDEF0100AB5001 /* Frameworks */, + D0001A07ABCDEF0100AB5001 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D0001A03ABCDEF0100AB5001 /* CalendarrWidgets */, + D0001A04ABCDEF0100AB5001 /* Shared */, + ); + name = CalendarrWidgets; + packageProductDependencies = ( + ); + productName = CalendarrWidgets; + productReference = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -135,6 +220,9 @@ C0000A01FB4E10100AB5001 = { CreatedOnToolsVersion = 26.4.1; }; + D0001A01ABCDEF0100AB5001 = { + CreatedOnToolsVersion = 26.5; + }; }; }; buildConfigurationList = C0000601FB4E10100AB5001 /* Build configuration list for PBXProject "Calendarr iOS" */; @@ -153,6 +241,7 @@ projectRoot = ""; targets = ( C0000A01FB4E10100AB5001 /* Calendarr iOS */, + D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */, 3927C7BB2FB99D0E00EAD8ED /* Calendarr iOSTests */, ); }; @@ -173,6 +262,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D0001A07ABCDEF0100AB5001 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -190,6 +286,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D0001A05ABCDEF0100AB5001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -198,6 +301,11 @@ target = C0000A01FB4E10100AB5001 /* Calendarr iOS */; targetProxy = 3927C7C02FB99D0F00EAD8ED /* PBXContainerItemProxy */; }; + D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */; + targetProxy = D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -206,22 +314,23 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = PP34X97WS3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.5; MACOSX_DEPLOYMENT_TARGET = 26.5; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = "com.local.scarriffle.Calendarr-iOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = NO; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Calendarr iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Calendarr iOS"; XROS_DEPLOYMENT_TARGET = 26.5; }; @@ -232,22 +341,23 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = PP34X97WS3; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.5; MACOSX_DEPLOYMENT_TARGET = 26.5; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = "com.local.scarriffle.Calendarr-iOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = NO; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Calendarr iOS.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Calendarr iOS"; XROS_DEPLOYMENT_TARGET = 26.5; }; @@ -379,14 +489,15 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Calendarr iOS/Calendarr iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = PP34X97WS3; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = Calendarr; INFOPLIST_KEY_CFBundleName = Calendarr; - "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption" = NO; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -398,7 +509,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios; PRODUCT_NAME = "Calendarr iOS"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -420,14 +531,15 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "Calendarr iOS/Calendarr iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = PP34X97WS3; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = Calendarr; INFOPLIST_KEY_CFBundleName = Calendarr; - "INFOPLIST_KEY_ITSAppUsesNonExemptEncryption" = NO; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsArbitraryLoads = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -439,7 +551,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios; PRODUCT_NAME = "Calendarr iOS"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -456,6 +568,75 @@ }; name = Release; }; + D0001A09ABCDEF0100AB5001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_ENTITLEMENTS = CalendarrWidgets/CalendarrWidgets.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = PP34X97WS3; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = CalendarrWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Calendarr Widgets"; + INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0.1; + PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.CalendarrWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + D0001A0AABCDEF0100AB5001 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_ENTITLEMENTS = CalendarrWidgets/CalendarrWidgets.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = PP34X97WS3; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = CalendarrWidgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Calendarr Widgets"; + INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0.1; + PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.CalendarrWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -486,6 +667,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D0001A08ABCDEF0100AB5001 /* Build configuration list for PBXNativeTarget "CalendarrWidgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0001A09ABCDEF0100AB5001 /* Debug */, + D0001A0AABCDEF0100AB5001 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = C0000301FB4E10100AB5001 /* Project object */; diff --git a/Calendarr iOS/Views/Calendar/CalendarHostView.swift b/Calendarr iOS/Views/Calendar/CalendarHostView.swift index 3fb638e..aaa9f5c 100644 --- a/Calendarr iOS/Views/Calendar/CalendarHostView.swift +++ b/Calendarr iOS/Views/Calendar/CalendarHostView.swift @@ -15,6 +15,7 @@ struct CalendarHostView: View { @State private var editingEvent: CalEvent? = nil @State private var selectedEvent: CalEvent? = nil @State private var visibleMonth: Date = .now + @State private var showFilter = false private var titleString: String { if store.viewType == .month { @@ -67,6 +68,9 @@ struct CalendarHostView: View { .onChange(of: store.viewType) { _, _ in Task { await onNavigate() } } .onChange(of: cacheMonths) { _, _ in Task { await recache() } } .onChange(of: visibleMonth) { _, new in Task { await ensureLoaded(around: new) } } + .onReceive(NotificationCenter.default.publisher(for: .banishedCalendarsChanged)) { _ in + store.syncBanishedFromDefaults() + } } // MARK: – Liquid Glass variant @@ -102,6 +106,11 @@ struct CalendarHostView: View { ToolbarItem(placement: .navigationBarTrailing) { HStack(spacing: 8) { viewPickerMenu + Button { showFilter = true } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .foregroundStyle(store.hiddenCalendarKeys.isEmpty ? .primary : Color.accentColor) + } + .accessibilityLabel(L10n.t("filter.button", appLang)) Button { showMenu = true } label: { Image(systemName: "line.3.horizontal") } } } @@ -114,6 +123,9 @@ struct CalendarHostView: View { .onChange(of: store.viewType) { _, _ in Task { await onNavigate() } } .onChange(of: cacheMonths) { _, _ in Task { await recache() } } .onChange(of: visibleMonth) { _, new in Task { await ensureLoaded(around: new) } } + .onReceive(NotificationCenter.default.publisher(for: .banishedCalendarsChanged)) { _ in + store.syncBanishedFromDefaults() + } } // MARK: – Top bar (flat mode) @@ -142,6 +154,7 @@ struct CalendarHostView: View { .minimumScaleFactor(0.7) Spacer(minLength: 8) viewPickerMenu + filterButton Button { showMenu = true } label: { Image(systemName: "line.3.horizontal") .font(.system(size: 18, weight: .medium)) @@ -153,6 +166,16 @@ struct CalendarHostView: View { .background(.bar) } + private var filterButton: some View { + Button { showFilter = true } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .font(.system(size: 17, weight: .medium)) + .foregroundStyle(store.hiddenCalendarKeys.isEmpty ? .primary : Color.accentColor) + .frame(width: 40, height: 40) + } + .accessibilityLabel(L10n.t("filter.button", appLang)) + } + private var viewPickerMenu: some View { Menu { ForEach(CalViewType.allCases, id: \.self) { vt in @@ -298,8 +321,8 @@ struct CalendarHostView: View { private var calendarSheets: CalendarSheets { CalendarSheets(store: store, showEditor: $showEditor, editorDate: $editorDate, editingEvent: $editingEvent, - selectedEvent: $selectedEvent, api: api, - reload: { await onNavigate() }) + selectedEvent: $selectedEvent, showFilter: $showFilter, + api: api, reload: { await onNavigate() }) } // MARK: – Loading logic @@ -327,13 +350,21 @@ struct CalendarHostView: View { await startup() } - /// Called when the user scrolls into a new month – fetches a ±1 month window - /// around it on demand. `loadEvents` skips the network if cached. + /// Called when the user scrolls into a new month – refreshes the visible range + /// immediately from cache, then fetches on demand if needed. private func ensureLoaded(around month: Date) async { let cal = store.userCalendar let monthStart = cal.date(from: cal.dateComponents([.year, .month], from: month)) ?? month let s = cal.date(byAdding: .month, value: -1, to: monthStart) ?? monthStart let e = cal.date(byAdding: .month, value: 2, to: monthStart) ?? monthStart + + // Only narrow the visible event set if the requested window is already cached. + // Otherwise keep the current events visible until the network fetch finishes, + // so previous/current/next month events don't disappear temporarily. + if store.isCached(start: s, end: e) { + store.refreshFromCache(start: s, end: e) + } + await store.loadEvents(api: api, start: s, end: e) } } @@ -346,6 +377,7 @@ private struct CalendarSheets: ViewModifier { @Binding var editorDate: Date @Binding var editingEvent: CalEvent? @Binding var selectedEvent: CalEvent? + @Binding var showFilter: Bool let api: CalendarrAPI let reload: () async -> Void @@ -364,5 +396,8 @@ private struct CalendarSheets: ViewModifier { await reload() } } + .sheet(isPresented: $showFilter) { + CalendarFilterSheet(api: api, store: store) + } } } diff --git a/Calendarr iOS/Views/Calendar/MonthView.swift b/Calendarr iOS/Views/Calendar/MonthView.swift index 8413407..f4f64c8 100644 --- a/Calendarr iOS/Views/Calendar/MonthView.swift +++ b/Calendarr iOS/Views/Calendar/MonthView.swift @@ -1,7 +1,9 @@ import SwiftUI -private let weeksBack = 104 -private let weeksAhead = 104 +// Cover ~10 years back and ~77 years ahead, so the scrollable range goes well +// past 2100 — enough room for any vacation that's actually getting planned. +private let weeksBack = 520 +private let weeksAhead = 4000 private let weekdayHeaderHeight: CGFloat = 28 private let dayNumberRowHeight: CGFloat = 22 private let laneHeight: CGFloat = 16 @@ -106,13 +108,24 @@ struct MonthView: View { cal.date(from: cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date))! } - /// Treat the visible month as the one that "owns" Thursday of the current week — - /// matches ISO week-month conventions and avoids flicker on month boundaries. + /// Determine the visible month from the currently-scrolled week. + /// Instead of switching as soon as a few days of the next month appear, + /// we count the month affiliation of the visible week rows and keep the + /// month that occupies the majority of the viewport. private func publishVisibleMonth(from week: Date?) { guard let w = week else { return } - let thursday = cal.date(byAdding: .day, value: 3, to: w) ?? w - let m = cal.date(from: cal.dateComponents([.year, .month], from: thursday)) ?? thursday - if visibleMonth != m { visibleMonth = m } + + let visibleWeeks = (0..<6).compactMap { cal.date(byAdding: .weekOfYear, value: $0, to: w) } + let monthCounts = visibleWeeks.reduce(into: [Date: Int]()) { acc, weekStart in + guard let midWeek = cal.date(byAdding: .day, value: 3, to: weekStart) else { return } + let month = cal.date(from: cal.dateComponents([.year, .month], from: midWeek)) ?? midWeek + acc[month, default: 0] += 1 + } + + let selectedMonth = monthCounts.max { a, b in a.value < b.value }?.key + if let m = selectedMonth, visibleMonth != m { + visibleMonth = m + } } }