Compare commits
6 Commits
554ad425b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1da8de06a | ||
|
|
84f5c00516 | ||
|
|
0d2ad6e021 | ||
|
|
7fa8dbdbdf | ||
|
|
c9bd42aae5 | ||
|
|
781ccd1752 |
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000001 /* CalendarrCore */; };
|
||||||
|
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000002 /* CalendarrCore */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -95,6 +97,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -102,6 +105,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -175,6 +179,7 @@
|
|||||||
);
|
);
|
||||||
name = "Calendarr iOS";
|
name = "Calendarr iOS";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
CA1E4D0A0001000000000001 /* CalendarrCore */,
|
||||||
);
|
);
|
||||||
productName = "Calendarr iOS";
|
productName = "Calendarr iOS";
|
||||||
productReference = C0000B01FC4E10100AB5001 /* Calendarr iOS.app */;
|
productReference = C0000B01FC4E10100AB5001 /* Calendarr iOS.app */;
|
||||||
@@ -198,6 +203,7 @@
|
|||||||
);
|
);
|
||||||
name = CalendarrWidgets;
|
name = CalendarrWidgets;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
CA1E4D0A0001000000000002 /* CalendarrCore */,
|
||||||
);
|
);
|
||||||
productName = CalendarrWidgets;
|
productName = CalendarrWidgets;
|
||||||
productReference = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */;
|
productReference = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */;
|
||||||
@@ -235,6 +241,9 @@
|
|||||||
);
|
);
|
||||||
mainGroup = C0000201FB4E10100AB5001;
|
mainGroup = C0000201FB4E10100AB5001;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
39450EFC302A266A0090DDD7 /* XCLocalSwiftPackageReference "../CalendarrKit" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = C0000C01FB4E10100AB5001 /* Products */;
|
productRefGroup = C0000C01FB4E10100AB5001 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -314,12 +323,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.5;
|
MACOSX_DEPLOYMENT_TARGET = 26.5;
|
||||||
MARKETING_VERSION = 1.0.1;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.local.scarriffle.Calendarr-iOSTests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.local.scarriffle.Calendarr-iOSTests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
@@ -341,12 +348,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.5;
|
MACOSX_DEPLOYMENT_TARGET = 26.5;
|
||||||
MARKETING_VERSION = 1.0.1;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.local.scarriffle.Calendarr-iOSTests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.local.scarriffle.Calendarr-iOSTests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
@@ -397,6 +402,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
@@ -415,8 +421,9 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -461,6 +468,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -473,8 +481,9 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 3.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -490,9 +499,10 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Calendarr iOS/Calendarr iOS.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Calendarr iOS/Calendarr iOS.entitlements";
|
||||||
|
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "Calendarr iOS/Calendarr iOS-Catalyst.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
@@ -506,18 +516,16 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 3.5;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
||||||
PRODUCT_NAME = "Calendarr iOS";
|
PRODUCT_NAME = "Calendarr iOS";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
@@ -526,6 +534,7 @@
|
|||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -535,9 +544,10 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Calendarr iOS/Calendarr iOS.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Calendarr iOS/Calendarr iOS.entitlements";
|
||||||
|
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "Calendarr iOS/Calendarr iOS-Catalyst.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
@@ -551,18 +561,16 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 3.5;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
|
||||||
PRODUCT_NAME = "Calendarr iOS";
|
PRODUCT_NAME = "Calendarr iOS";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
@@ -571,6 +579,7 @@
|
|||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -579,26 +588,25 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = CalendarrWidgets/CalendarrWidgets.entitlements;
|
CODE_SIGN_ENTITLEMENTS = CalendarrWidgets/CalendarrWidgets.entitlements;
|
||||||
|
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "CalendarrWidgets/CalendarrWidgets-Catalyst.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = CalendarrWidgets/Info.plist;
|
INFOPLIST_FILE = CalendarrWidgets/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Calendarr Widgets";
|
INFOPLIST_KEY_CFBundleDisplayName = "Calendarr Widgets";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.CalendarrWidgets;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.CalendarrWidgets;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
@@ -606,6 +614,7 @@
|
|||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -614,32 +623,32 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = CalendarrWidgets/CalendarrWidgets.entitlements;
|
CODE_SIGN_ENTITLEMENTS = CalendarrWidgets/CalendarrWidgets.entitlements;
|
||||||
|
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "CalendarrWidgets/CalendarrWidgets-Catalyst.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = CalendarrWidgets/Info.plist;
|
INFOPLIST_FILE = CalendarrWidgets/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Calendarr Widgets";
|
INFOPLIST_KEY_CFBundleDisplayName = "Calendarr Widgets";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "© 2026 Scarriffleservices";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.1;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.CalendarrWidgets;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.CalendarrWidgets;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = YES;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -683,6 +692,24 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
CA1E4D0A0001000000000001 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
|
CA1E4D0A0001000000000002 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
39450EFC302A266A0090DDD7 /* XCLocalSwiftPackageReference "../CalendarrKit" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = ../CalendarrKit;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
};
|
};
|
||||||
rootObject = C0000301FB4E10100AB5001 /* Project object */;
|
rootObject = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<key>CalendarrWidgets.xcscheme_^#shared#^_</key>
|
<key>CalendarrWidgets.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
|||||||
46
Calendarr iOS/Calendarr iOS-Catalyst.entitlements
Normal file
46
Calendarr iOS/Calendarr iOS-Catalyst.entitlements
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!--
|
||||||
|
Mac Catalyst entitlements. A separate file is required because the App
|
||||||
|
Group value differs per platform: macOS demands the Team ID prefix, iOS
|
||||||
|
forbids it, and listing both in one file breaks iOS provisioning.
|
||||||
|
Selected via CODE_SIGN_ENTITLEMENTS[sdk=macosx*].
|
||||||
|
|
||||||
|
This one file serves both distribution paths. app-sandbox is required by
|
||||||
|
the Mac App Store and desirable for Developer ID; ENABLE_HARDENED_RUNTIME
|
||||||
|
is required for notarization and ignored by the App Store. Only the
|
||||||
|
signing identity differs, and that is chosen at export time.
|
||||||
|
-->
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Talks to the user's own Calendarr server. -->
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- BirthdaysImporter reads Contacts to sync birthdays. -->
|
||||||
|
<key>com.apple.security.personal-information.addressbook</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- .ics import (fileImporter) and calendar export (fileExporter). -->
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Must match CalendarrAppGroup.current on this platform, byte for byte. -->
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>PP34X97WS3.group.com.scarriffleservices.calendarr</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<!-- Own group first: it stays the default access group, so tokens written
|
||||||
|
before this entitlement existed keep resolving. The .shared group is
|
||||||
|
what a second app from this team reads the auth token from. -->
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.scarriffleservices.calendarr.ios</string>
|
||||||
|
<string>$(AppIdentifierPrefix)com.scarriffleservices.calendarr.shared</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -6,5 +6,15 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>group.com.scarriffleservices.calendarr</string>
|
<string>group.com.scarriffleservices.calendarr</string>
|
||||||
</array>
|
</array>
|
||||||
|
<!-- Order matters. Without this entitlement the default access group is
|
||||||
|
<AppIdentifierPrefix><bundle id>, which is where every existing token
|
||||||
|
lives. Adding the entitlement makes the FIRST entry the new default, so
|
||||||
|
listing our own group first keeps those tokens resolving and nobody
|
||||||
|
gets signed out. The .shared group is the one the Mac app also reads. -->
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)com.scarriffleservices.calendarr.ios</string>
|
||||||
|
<string>$(AppIdentifierPrefix)com.scarriffleservices.calendarr.shared</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Security
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct CalendarrApp: App {
|
struct CalendarrApp: App {
|
||||||
@@ -25,17 +24,45 @@ class AppState {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
serverURL = UserDefaults.standard.string(forKey: "serverURL") ?? ""
|
serverURL = UserDefaults.standard.string(forKey: "serverURL") ?? ""
|
||||||
// Migrate a token previously kept in UserDefaults into the Keychain once,
|
authToken = Self.loadToken()
|
||||||
// so existing logins survive the change without re-authenticating.
|
|
||||||
if let legacy = UserDefaults.standard.string(forKey: "authToken"), !legacy.isEmpty {
|
|
||||||
Keychain.set(legacy, for: "authToken")
|
|
||||||
UserDefaults.standard.removeObject(forKey: "authToken")
|
|
||||||
}
|
|
||||||
authToken = Keychain.get("authToken") ?? ""
|
|
||||||
username = UserDefaults.standard.string(forKey: "username") ?? ""
|
username = UserDefaults.standard.string(forKey: "username") ?? ""
|
||||||
isAdmin = UserDefaults.standard.bool(forKey: "isAdmin")
|
isAdmin = UserDefaults.standard.bool(forKey: "isAdmin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the stored token, migrating it forward from wherever an older build
|
||||||
|
/// left it. Runs on every launch but does real work only once.
|
||||||
|
///
|
||||||
|
/// Step 2 is the one that keeps existing users signed in. Before the
|
||||||
|
/// `keychain-access-groups` entitlement existed, items landed in the app's
|
||||||
|
/// own default group. Adding the entitlement makes the *first* array entry
|
||||||
|
/// the new default — and that entry is deliberately the app's own group, so
|
||||||
|
/// an unqualified query still resolves those items and we can copy them
|
||||||
|
/// across instead of stranding them.
|
||||||
|
private static func loadToken() -> String {
|
||||||
|
let key = "authToken"
|
||||||
|
|
||||||
|
// 1. Already in the shared group — the steady state.
|
||||||
|
if let token = try? KeychainStore.get(key), !token.isEmpty {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. In the app's own default group, written before the entitlement.
|
||||||
|
if let token = try? KeychainStore.get(key, accessGroup: nil), !token.isEmpty {
|
||||||
|
try? KeychainStore.set(token, for: key)
|
||||||
|
try? KeychainStore.set(nil, for: key, accessGroup: nil)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. In UserDefaults, written before secrets moved to the Keychain.
|
||||||
|
if let legacy = UserDefaults.standard.string(forKey: key), !legacy.isEmpty {
|
||||||
|
try? KeychainStore.set(legacy, for: key)
|
||||||
|
UserDefaults.standard.removeObject(forKey: key)
|
||||||
|
return legacy
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func saveServer(url: String) {
|
func saveServer(url: String) {
|
||||||
serverURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
serverURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if serverURL.hasSuffix("/") { serverURL = String(serverURL.dropLast()) }
|
if serverURL.hasSuffix("/") { serverURL = String(serverURL.dropLast()) }
|
||||||
@@ -46,58 +73,45 @@ class AppState {
|
|||||||
authToken = token
|
authToken = token
|
||||||
username = user
|
username = user
|
||||||
isAdmin = admin
|
isAdmin = admin
|
||||||
Keychain.set(token, for: "authToken") // secret → Keychain, not UserDefaults
|
try? KeychainStore.set(token, for: "authToken") // secret → Keychain, not UserDefaults
|
||||||
UserDefaults.standard.set(user, forKey: "username")
|
UserDefaults.standard.set(user, forKey: "username")
|
||||||
UserDefaults.standard.set(admin, forKey: "isAdmin")
|
UserDefaults.standard.set(admin, forKey: "isAdmin")
|
||||||
|
publishSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirror the non-secret session facts into the shared container. Apps in
|
||||||
|
/// another sandbox cannot read our UserDefaults, so this is how they learn
|
||||||
|
/// which server we point at and whether anyone is signed in. The token
|
||||||
|
/// itself stays in the shared keychain group, never in a plain file.
|
||||||
|
private func publishSession() {
|
||||||
|
WidgetStore.writeSession(baseURL: serverURL,
|
||||||
|
username: username,
|
||||||
|
isLoggedIn: isLoggedIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logout() {
|
func logout() {
|
||||||
authToken = ""
|
authToken = ""
|
||||||
username = ""
|
username = ""
|
||||||
isAdmin = false
|
isAdmin = false
|
||||||
Keychain.set(nil, for: "authToken")
|
try? KeychainStore.set(nil, for: "authToken")
|
||||||
UserDefaults.standard.removeObject(forKey: "authToken") // clear any legacy copy
|
try? KeychainStore.set(nil, for: "authToken", accessGroup: nil) // pre-entitlement copy
|
||||||
|
UserDefaults.standard.removeObject(forKey: "authToken") // pre-Keychain copy
|
||||||
UserDefaults.standard.removeObject(forKey: "username")
|
UserDefaults.standard.removeObject(forKey: "username")
|
||||||
UserDefaults.standard.removeObject(forKey: "isAdmin")
|
UserDefaults.standard.removeObject(forKey: "isAdmin")
|
||||||
|
// The shared container outlives the session, so it has to be cleared
|
||||||
|
// explicitly — otherwise widgets keep showing the signed-out user's data.
|
||||||
|
// The session record stays behind, flagged signed-out, so a reader can
|
||||||
|
// say "sign in to Calendarr" rather than "open Calendarr once".
|
||||||
|
WidgetStore.clear()
|
||||||
|
publishSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetServer() {
|
func resetServer() {
|
||||||
logout()
|
logout()
|
||||||
serverURL = ""
|
serverURL = ""
|
||||||
UserDefaults.standard.removeObject(forKey: "serverURL")
|
UserDefaults.standard.removeObject(forKey: "serverURL")
|
||||||
}
|
// Back to unconfigured: there is no server left to name, so drop the
|
||||||
}
|
// session record rather than leaving a stale URL in the container.
|
||||||
|
WidgetStore.clearSession()
|
||||||
/// Minimal Keychain wrapper for secrets (the auth bearer token). Values are
|
|
||||||
/// stored as generic passwords, accessible after first unlock.
|
|
||||||
enum Keychain {
|
|
||||||
private static let service = "Calendarr"
|
|
||||||
|
|
||||||
static func set(_ value: String?, for key: String) {
|
|
||||||
let base: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecAttrAccount as String: key,
|
|
||||||
]
|
|
||||||
SecItemDelete(base as CFDictionary)
|
|
||||||
guard let value, let data = value.data(using: .utf8) else { return }
|
|
||||||
var add = base
|
|
||||||
add[kSecValueData as String] = data
|
|
||||||
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
|
||||||
SecItemAdd(add as CFDictionary, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func get(_ key: String) -> String? {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrService as String: service,
|
|
||||||
kSecAttrAccount as String: key,
|
|
||||||
kSecReturnData as String: true,
|
|
||||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
||||||
]
|
|
||||||
var out: AnyObject?
|
|
||||||
guard SecItemCopyMatching(query as CFDictionary, &out) == errSecSuccess,
|
|
||||||
let data = out as? Data else { return nil }
|
|
||||||
return String(data: data, encoding: .utf8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
/// Posted whenever the persistent "banished calendars" set is mutated from
|
/// Posted whenever the persistent "banished calendars" set is mutated from
|
||||||
@@ -579,8 +580,12 @@ class CalendarStore {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
// Include the week before today so widgets that show the current week
|
// Include the week before today so widgets that show the current week
|
||||||
// (e.g. "This Week", "Up Next + Calendar") have data for Monday–today.
|
// (e.g. "This Week", "Up Next + Calendar") have data for Monday–today.
|
||||||
let from = cal.date(byAdding: .day, value: -7, to: cal.startOfDay(for: now)) ?? now
|
// The window is published in the snapshot as coverageStart/coverageEnd:
|
||||||
let to = cal.date(byAdding: .day, value: 42, to: cal.startOfDay(for: now)) ?? from
|
// outside it a reader has no information, which is not the same as
|
||||||
|
// having no events, and only the writer knows where the edge is.
|
||||||
|
let dayStart = cal.startOfDay(for: now)
|
||||||
|
let from = cal.date(byAdding: .day, value: -SnapshotCoverage.daysBehind, to: dayStart) ?? now
|
||||||
|
let to = cal.date(byAdding: .day, value: SnapshotCoverage.daysAhead, to: dayStart) ?? from
|
||||||
|
|
||||||
// Build the calendar list from all cached events (not just the window)
|
// Build the calendar list from all cached events (not just the window)
|
||||||
// so every calendar appears in the widget configuration picker.
|
// so every calendar appears in the widget configuration picker.
|
||||||
@@ -604,7 +609,7 @@ class CalendarStore {
|
|||||||
&& !banishedCalendarKeys.contains(key)
|
&& !banishedCalendarKeys.contains(key)
|
||||||
}
|
}
|
||||||
.sorted { $0.startDate < $1.startDate }
|
.sorted { $0.startDate < $1.startDate }
|
||||||
.prefix(500)
|
.prefix(SnapshotCoverage.maxEvents)
|
||||||
.map { ev in
|
.map { ev in
|
||||||
WidgetEvent(id: ev.id,
|
WidgetEvent(id: ev.id,
|
||||||
title: ev.title,
|
title: ev.title,
|
||||||
@@ -615,19 +620,9 @@ class CalendarStore {
|
|||||||
location: ev.location,
|
location: ev.location,
|
||||||
calendarKey: Self.calendarKey(source: ev.source, calendarId: ev.calendarId))
|
calendarKey: Self.calendarKey(source: ev.source, calendarId: ev.calendarId))
|
||||||
}
|
}
|
||||||
let defaults = UserDefaults.standard
|
WidgetStore.write(WidgetStore.makeSnapshot(events: Array(visible),
|
||||||
let snap = WidgetSnapshot(
|
coverageStart: from,
|
||||||
writtenAt: now,
|
coverageEnd: to))
|
||||||
events: Array(visible),
|
|
||||||
todayColorHex: defaults.string(forKey: "todayColor") ?? "#4285f4",
|
|
||||||
textColorHex: defaults.string(forKey: "textColor") ?? "#FFFFFF",
|
|
||||||
backgroundColorHex: defaults.string(forKey: "backgroundColor") ?? "#000000",
|
|
||||||
lineColorHex: defaults.string(forKey: "lineColor") ?? "#3A3A52",
|
|
||||||
primaryColorHex: defaults.string(forKey: "primaryColor") ?? "#4285f4",
|
|
||||||
accentColorHex: defaults.string(forKey: "accentColor") ?? "#ea4335",
|
|
||||||
language: defaults.string(forKey: "appLanguage") ?? "system"
|
|
||||||
)
|
|
||||||
WidgetStore.write(snap)
|
|
||||||
WidgetTimelineNotifier.reload()
|
WidgetTimelineNotifier.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
131
Calendarr iOS/Services/KeychainStore.swift
Normal file
131
Calendarr iOS/Services/KeychainStore.swift
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
/// Keychain access for secrets (currently just the auth bearer token).
|
||||||
|
///
|
||||||
|
/// This replaces the earlier `enum Keychain`, which discarded every `OSStatus`.
|
||||||
|
/// That mattered more than it looks: on Mac Catalyst a missing
|
||||||
|
/// `keychain-access-groups` entitlement makes `SecItemAdd`/`SecItemCopyMatching`
|
||||||
|
/// fail with `errSecMissingEntitlement`, and because nothing checked the status
|
||||||
|
/// the result was indistinguishable from "no token stored" — the user was
|
||||||
|
/// silently signed out on every launch.
|
||||||
|
///
|
||||||
|
/// Items live in a shared access group so the Mac build and other apps from the
|
||||||
|
/// same team can read the token. Reading requires `keychain-access-groups` in
|
||||||
|
/// the entitlements listing `sharedAccessGroup`.
|
||||||
|
enum KeychainStore {
|
||||||
|
/// Access group shared across our apps. Must match an entry in the
|
||||||
|
/// `keychain-access-groups` entitlement of every app that uses it.
|
||||||
|
/// The `PP34X97WS3.` prefix is this account's Team ID — the same value
|
||||||
|
/// `$(AppIdentifierPrefix)` expands to at build time.
|
||||||
|
static let sharedAccessGroup = "PP34X97WS3.com.scarriffleservices.calendarr.shared"
|
||||||
|
|
||||||
|
private static let service = "Calendarr"
|
||||||
|
|
||||||
|
/// Set when a keychain call failed because the entitlement was missing and
|
||||||
|
/// we fell back to the app's own default access group. Signing is a build
|
||||||
|
/// configuration concern, so in DEBUG we make it loud; in release we keep
|
||||||
|
/// working rather than locking the user out of their own account.
|
||||||
|
private(set) nonisolated(unsafe) static var didFallBackToDefaultGroup = false
|
||||||
|
|
||||||
|
// MARK: - Public API
|
||||||
|
|
||||||
|
/// Store or delete a value. Passing `nil` deletes it.
|
||||||
|
static func set(_ value: String?, for key: String,
|
||||||
|
accessGroup: String? = sharedAccessGroup) throws {
|
||||||
|
do {
|
||||||
|
try write(value, key: key, accessGroup: accessGroup)
|
||||||
|
} catch KeychainError.status(errSecMissingEntitlement, _) where accessGroup != nil {
|
||||||
|
noteEntitlementFallback()
|
||||||
|
try write(value, key: key, accessGroup: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a value. Returns `nil` when the item simply is not there;
|
||||||
|
/// throws when the keychain itself refused the request.
|
||||||
|
static func get(_ key: String, accessGroup: String? = sharedAccessGroup) throws -> String? {
|
||||||
|
do {
|
||||||
|
return try read(key: key, accessGroup: accessGroup)
|
||||||
|
} catch KeychainError.status(errSecMissingEntitlement, _) where accessGroup != nil {
|
||||||
|
noteEntitlementFallback()
|
||||||
|
return try read(key: key, accessGroup: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Implementation
|
||||||
|
|
||||||
|
private static func baseQuery(key: String, accessGroup: String?) -> [String: Any] {
|
||||||
|
var q: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
// Selects the modern, entitlement-gated keychain on macOS/Catalyst
|
||||||
|
// instead of the legacy file-based login keychain. Documented no-op
|
||||||
|
// on iOS 13+, so it is safe to set unconditionally.
|
||||||
|
kSecUseDataProtectionKeychain as String: true,
|
||||||
|
]
|
||||||
|
if let accessGroup { q[kSecAttrAccessGroup as String] = accessGroup }
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func write(_ value: String?, key: String, accessGroup: String?) throws {
|
||||||
|
let base = baseQuery(key: key, accessGroup: accessGroup)
|
||||||
|
|
||||||
|
let deleteStatus = SecItemDelete(base as CFDictionary)
|
||||||
|
guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else {
|
||||||
|
throw KeychainError.status(deleteStatus, operation: "delete \(key)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let value, let data = value.data(using: .utf8) else { return }
|
||||||
|
|
||||||
|
var add = base
|
||||||
|
add[kSecValueData as String] = data
|
||||||
|
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||||
|
|
||||||
|
let addStatus = SecItemAdd(add as CFDictionary, nil)
|
||||||
|
guard addStatus == errSecSuccess else {
|
||||||
|
throw KeychainError.status(addStatus, operation: "add \(key)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func read(key: String, accessGroup: String?) throws -> String? {
|
||||||
|
var query = baseQuery(key: key, accessGroup: accessGroup)
|
||||||
|
query[kSecReturnData as String] = true
|
||||||
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||||
|
|
||||||
|
var out: AnyObject?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &out)
|
||||||
|
switch status {
|
||||||
|
case errSecSuccess:
|
||||||
|
guard let data = out as? Data else { return nil }
|
||||||
|
return String(data: data, encoding: .utf8)
|
||||||
|
case errSecItemNotFound:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
throw KeychainError.status(status, operation: "read \(key)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func noteEntitlementFallback() {
|
||||||
|
guard !didFallBackToDefaultGroup else { return }
|
||||||
|
didFallBackToDefaultGroup = true
|
||||||
|
assertionFailure("""
|
||||||
|
Keychain access group \(sharedAccessGroup) was refused \
|
||||||
|
(errSecMissingEntitlement). Add it to keychain-access-groups and \
|
||||||
|
enable Keychain Sharing on the App ID. Falling back to the app's \
|
||||||
|
own default group — the Mac app will not see this token.
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeychainError: Error, LocalizedError {
|
||||||
|
case status(OSStatus, operation: String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case let .status(status, operation):
|
||||||
|
let detail = SecCopyErrorMessageString(status, nil) as String? ?? "OSStatus \(status)"
|
||||||
|
return "Keychain \(operation) failed: \(detail)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,11 +32,16 @@ struct CalendarEntityQuery: EntityQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Widget configuration intent: lets the user pick which calendars to show.
|
/// Widget configuration intent: lets the user pick which calendars to show.
|
||||||
/// An empty selection means "show all calendars" (the default).
|
/// No selection means "show all calendars" (the default).
|
||||||
|
///
|
||||||
|
/// The parameter must be optional: WidgetConfigurationIntent requires every
|
||||||
|
/// parameter type to be optional, and the macOS/Mac Catalyst SDK enforces that
|
||||||
|
/// where the iOS one lets a bare array through. `nil` and `[]` are treated
|
||||||
|
/// alike downstream, so this is behaviour-preserving.
|
||||||
struct CalendarSelectionIntent: WidgetConfigurationIntent {
|
struct CalendarSelectionIntent: WidgetConfigurationIntent {
|
||||||
static var title: LocalizedStringResource = "Kalender auswählen"
|
static var title: LocalizedStringResource = "Kalender auswählen"
|
||||||
static var description = IntentDescription("Wähle welche Kalender im Widget angezeigt werden. Leer = alle Kalender.")
|
static var description = IntentDescription("Wähle welche Kalender im Widget angezeigt werden. Leer = alle Kalender.")
|
||||||
|
|
||||||
@Parameter(title: "Kalender")
|
@Parameter(title: "Kalender")
|
||||||
var selectedCalendars: [CalendarAppEntity]
|
var selectedCalendars: [CalendarAppEntity]?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,19 +38,20 @@ struct CalendarrTimelineProvider: AppIntentTimelineProvider {
|
|||||||
|
|
||||||
private func filtered(_ snapshot: WidgetSnapshot?, by config: CalendarSelectionIntent) -> WidgetSnapshot? {
|
private func filtered(_ snapshot: WidgetSnapshot?, by config: CalendarSelectionIntent) -> WidgetSnapshot? {
|
||||||
guard let snapshot else { return nil }
|
guard let snapshot else { return nil }
|
||||||
let ids = config.selectedCalendars.map { $0.id }
|
let ids = (config.selectedCalendars ?? []).map { $0.id }
|
||||||
guard !ids.isEmpty else { return snapshot } // empty = show all
|
guard !ids.isEmpty else { return snapshot } // nothing selected = show all
|
||||||
let keep = Set(ids)
|
let keep = Set(ids)
|
||||||
|
// Filtering narrows the events but not the window: the snapshot still
|
||||||
|
// speaks for the same range, it just has fewer calendars in it.
|
||||||
return WidgetSnapshot(
|
return WidgetSnapshot(
|
||||||
writtenAt: snapshot.writtenAt,
|
writtenAt: snapshot.writtenAt,
|
||||||
events: snapshot.events.filter { keep.contains($0.calendarKey) },
|
coverageStart: snapshot.coverageStart,
|
||||||
todayColorHex: snapshot.todayColorHex,
|
coverageEnd: snapshot.coverageEnd,
|
||||||
textColorHex: snapshot.textColorHex,
|
isLoggedIn: snapshot.isLoggedIn,
|
||||||
backgroundColorHex: snapshot.backgroundColorHex,
|
writerVersion: snapshot.writerVersion,
|
||||||
lineColorHex: snapshot.lineColorHex,
|
events: snapshot.events.filter { keep.contains($0.calendarKey) },
|
||||||
primaryColorHex: snapshot.primaryColorHex,
|
theme: snapshot.theme,
|
||||||
accentColorHex: snapshot.accentColorHex,
|
language: snapshot.language
|
||||||
language: snapshot.language
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
CalendarrWidgets/CalendarrWidgets-Catalyst.entitlements
Normal file
19
CalendarrWidgets/CalendarrWidgets-Catalyst.entitlements
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!--
|
||||||
|
Mac Catalyst entitlements for the widget extension. Deliberately minimal:
|
||||||
|
the widget only ever reads the snapshot from the App Group container. It
|
||||||
|
does no networking, touches no Contacts, and needs no keychain access.
|
||||||
|
-->
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- Must match CalendarrAppGroup.current on this platform, byte for byte. -->
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>PP34X97WS3.group.com.scarriffleservices.calendarr</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,178 +1,136 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
@_spi(Writer) import CalendarrCore
|
||||||
#if canImport(WidgetKit)
|
#if canImport(WidgetKit)
|
||||||
import WidgetKit
|
import WidgetKit
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// App-Group identifier shared between the main app and the widget extension.
|
// The snapshot types and the App Group plumbing now live in CalendarrCore, so
|
||||||
/// IMPORTANT: This must match the App Group capability in BOTH targets
|
// that a second app from this team reads the exact same format instead of
|
||||||
/// and the App Group ID registered in the Apple Developer Portal.
|
// reimplementing it and drifting.
|
||||||
let widgetAppGroupID = "group.com.scarriffleservices.calendarr"
|
//
|
||||||
|
// These aliases keep the ~70 existing call sites in the app and the widget
|
||||||
|
// compiling unchanged. They are a migration convenience, not a design: new code
|
||||||
|
// should use the CalendarrCore names directly.
|
||||||
|
typealias WidgetEvent = SnapshotEvent
|
||||||
|
typealias WidgetCalendar = SnapshotCalendar
|
||||||
|
typealias WidgetSnapshot = CalendarrSnapshot
|
||||||
|
|
||||||
/// Lightweight calendar descriptor stored alongside the event cache so the
|
/// The App Group identifier for this platform. See `CalendarrAppGroup`.
|
||||||
/// widget configuration intent can offer calendar options without a network call.
|
let widgetAppGroupID = CalendarrAppGroup.current
|
||||||
struct WidgetCalendar: Codable, Identifiable, Hashable {
|
|
||||||
let id: String // calendarKey ("source-calendarId")
|
extension CalendarrSnapshot {
|
||||||
let name: String
|
// The six colours are stored flat on the wire for backwards compatibility
|
||||||
let colorHex: String
|
// but exposed as `theme` in the package. These keep the widget views, which
|
||||||
}
|
// read them individually, from having to change.
|
||||||
|
var todayColorHex: String { theme.today }
|
||||||
/// Lightweight event representation that lives inside the widget cache.
|
var textColorHex: String { theme.text }
|
||||||
/// We strip everything the widget doesn't need (notes, calendar IDs, URLs).
|
var backgroundColorHex: String { theme.background }
|
||||||
struct WidgetEvent: Codable, Hashable, Identifiable {
|
var lineColorHex: String { theme.line }
|
||||||
let id: String
|
var primaryColorHex: String { theme.primary }
|
||||||
let title: String
|
var accentColorHex: String { theme.accent }
|
||||||
let start: Date
|
|
||||||
let end: Date
|
|
||||||
let isAllDay: Bool
|
|
||||||
let colorHex: String
|
|
||||||
let location: String
|
|
||||||
let calendarKey: String // used for per-calendar filtering in widget config
|
|
||||||
|
|
||||||
init(id: String, title: String, start: Date, end: Date,
|
|
||||||
isAllDay: Bool, colorHex: String, location: String, calendarKey: String) {
|
|
||||||
self.id = id; self.title = title; self.start = start; self.end = end
|
|
||||||
self.isAllDay = isAllDay; self.colorHex = colorHex
|
|
||||||
self.location = location; self.calendarKey = calendarKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backward-compatible decoder: old caches have no calendarKey field.
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
id = try c.decode(String.self, forKey: .id)
|
|
||||||
title = try c.decode(String.self, forKey: .title)
|
|
||||||
start = try c.decode(Date.self, forKey: .start)
|
|
||||||
end = try c.decode(Date.self, forKey: .end)
|
|
||||||
isAllDay = try c.decode(Bool.self, forKey: .isAllDay)
|
|
||||||
colorHex = try c.decode(String.self, forKey: .colorHex)
|
|
||||||
location = try c.decode(String.self, forKey: .location)
|
|
||||||
calendarKey = try c.decodeIfPresent(String.self, forKey: .calendarKey) ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snapshot blob the app writes to the App-Group container and the widget reads.
|
|
||||||
struct WidgetSnapshot: Codable {
|
|
||||||
let writtenAt: Date
|
|
||||||
let events: [WidgetEvent]
|
|
||||||
/// Mirrors the user's chosen visual settings so the widget looks the same
|
|
||||||
/// as the app even when its own AppStorage in the extension is empty.
|
|
||||||
let todayColorHex: String
|
|
||||||
let textColorHex: String
|
|
||||||
let backgroundColorHex: String
|
|
||||||
let lineColorHex: String
|
|
||||||
let primaryColorHex: String
|
|
||||||
let accentColorHex: String
|
|
||||||
let language: String
|
|
||||||
|
|
||||||
init(writtenAt: Date,
|
|
||||||
events: [WidgetEvent],
|
|
||||||
todayColorHex: String,
|
|
||||||
textColorHex: String,
|
|
||||||
backgroundColorHex: String,
|
|
||||||
lineColorHex: String,
|
|
||||||
primaryColorHex: String,
|
|
||||||
accentColorHex: String,
|
|
||||||
language: String) {
|
|
||||||
self.writtenAt = writtenAt
|
|
||||||
self.events = events
|
|
||||||
self.todayColorHex = todayColorHex
|
|
||||||
self.textColorHex = textColorHex
|
|
||||||
self.backgroundColorHex = backgroundColorHex
|
|
||||||
self.lineColorHex = lineColorHex
|
|
||||||
self.primaryColorHex = primaryColorHex
|
|
||||||
self.accentColorHex = accentColorHex
|
|
||||||
self.language = language
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Custom decoder so older caches without the new colour fields still load.
|
|
||||||
init(from decoder: Decoder) throws {
|
|
||||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
writtenAt = try c.decode(Date.self, forKey: .writtenAt)
|
|
||||||
events = try c.decode([WidgetEvent].self, forKey: .events)
|
|
||||||
todayColorHex = try c.decode(String.self, forKey: .todayColorHex)
|
|
||||||
textColorHex = try c.decode(String.self, forKey: .textColorHex)
|
|
||||||
backgroundColorHex = try c.decode(String.self, forKey: .backgroundColorHex)
|
|
||||||
lineColorHex = try c.decode(String.self, forKey: .lineColorHex)
|
|
||||||
language = try c.decode(String.self, forKey: .language)
|
|
||||||
primaryColorHex = try c.decodeIfPresent(String.self, forKey: .primaryColorHex) ?? "#4285f4"
|
|
||||||
accentColorHex = try c.decodeIfPresent(String.self, forKey: .accentColorHex) ?? "#ea4335"
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case writtenAt, events, todayColorHex, textColorHex, backgroundColorHex
|
|
||||||
case lineColorHex, primaryColorHex, accentColorHex, language
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thin facade over `CalendarrCore.SnapshotStore`, preserving the static API the
|
||||||
|
/// app and widget already use. Errors are swallowed here exactly as before —
|
||||||
|
/// a failed widget cache write must never interrupt the user — but the store now
|
||||||
|
/// reports *why* a read failed, which is what `read()` discards and callers that
|
||||||
|
/// need the distinction should use `SnapshotStore` directly for.
|
||||||
enum WidgetStore {
|
enum WidgetStore {
|
||||||
private static let cacheFilename = "widget-cache.json"
|
private static let store = SnapshotStore()
|
||||||
private static let calendarsFilename = "widget-calendars.json"
|
private static let sessions = SharedSessionStore()
|
||||||
|
|
||||||
private static var containerURL: URL? {
|
/// Version of the app writing the cache, for diagnostics in the consumer.
|
||||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: widgetAppGroupID)
|
private static var writerVersion: String {
|
||||||
|
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var cacheURL: URL? {
|
|
||||||
containerURL?.appendingPathComponent(cacheFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static var calendarsURL: URL? {
|
|
||||||
containerURL?.appendingPathComponent(calendarsFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called by the app whenever the event cache changes.
|
|
||||||
static func write(_ snapshot: WidgetSnapshot) {
|
static func write(_ snapshot: WidgetSnapshot) {
|
||||||
guard let url = cacheURL else { return }
|
try? store.write(snapshot)
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.dateEncodingStrategy = .iso8601
|
|
||||||
if let data = try? encoder.encode(snapshot) {
|
|
||||||
try? data.write(to: url, options: .atomic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by the widget timeline provider to load the latest snapshot.
|
|
||||||
static func read() -> WidgetSnapshot? {
|
static func read() -> WidgetSnapshot? {
|
||||||
guard let url = cacheURL, let data = try? Data(contentsOf: url) else { return nil }
|
store.read().snapshot
|
||||||
let decoder = JSONDecoder()
|
|
||||||
decoder.dateDecodingStrategy = .iso8601
|
|
||||||
return try? decoder.decode(WidgetSnapshot.self, from: data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the list of available calendars so the widget intent can offer
|
|
||||||
/// calendar options for filtering without a network call.
|
|
||||||
static func writeCalendars(_ calendars: [WidgetCalendar]) {
|
static func writeCalendars(_ calendars: [WidgetCalendar]) {
|
||||||
guard let url = calendarsURL else { return }
|
try? store.writeCalendars(calendars)
|
||||||
if let data = try? JSONEncoder().encode(calendars) {
|
|
||||||
try? data.write(to: url, options: .atomic)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the calendar list written by the main app.
|
|
||||||
static func readCalendars() -> [WidgetCalendar] {
|
static func readCalendars() -> [WidgetCalendar] {
|
||||||
guard let url = calendarsURL, let data = try? Data(contentsOf: url) else { return [] }
|
store.readCalendars()
|
||||||
return (try? JSONDecoder().decode([WidgetCalendar].self, from: data)) ?? []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rewrite the existing snapshot with the latest colour / language values
|
/// Record which server we are pointed at and whether anyone is signed in, so
|
||||||
/// from UserDefaults. Used when the user tweaks an appearance setting and
|
/// a reading app can tell "signed out" from "never ran" without reaching into
|
||||||
/// we want the widgets to refresh immediately, without needing a new event
|
/// this app's UserDefaults, which lives in another sandbox.
|
||||||
/// sync. No-op if there's no cached snapshot yet.
|
static func writeSession(baseURL: String, username: String, isLoggedIn: Bool) {
|
||||||
|
try? sessions.write(SharedSession(baseURL: baseURL,
|
||||||
|
username: username,
|
||||||
|
isLoggedIn: isLoggedIn,
|
||||||
|
writtenAt: Date()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forget the session entirely. Used on server reset, where the app returns
|
||||||
|
/// to unconfigured and there is no longer a server worth naming.
|
||||||
|
static func clearSession() {
|
||||||
|
sessions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the cached calendar. Called on sign-out and server reset: the
|
||||||
|
/// container outlives the session, so without this every reader keeps
|
||||||
|
/// rendering the previous user's events.
|
||||||
|
static func clear() {
|
||||||
|
store.clear()
|
||||||
|
WidgetTimelineNotifier.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewrite the existing snapshot with the latest colour / language values so
|
||||||
|
/// widgets pick up an appearance change immediately, without waiting for the
|
||||||
|
/// next event sync. No-op when nothing is cached yet.
|
||||||
static func republishAppearanceOnly() {
|
static func republishAppearanceOnly() {
|
||||||
guard let existing = read() else { return }
|
guard let existing = read() else { return }
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
let updated = WidgetSnapshot(
|
let updated = WidgetSnapshot(
|
||||||
writtenAt: Date(),
|
writtenAt: Date(),
|
||||||
|
coverageStart: existing.coverageStart,
|
||||||
|
coverageEnd: existing.coverageEnd,
|
||||||
|
isLoggedIn: existing.isLoggedIn,
|
||||||
|
writerVersion: writerVersion,
|
||||||
events: existing.events,
|
events: existing.events,
|
||||||
todayColorHex: defaults.string(forKey: "todayColor") ?? existing.todayColorHex,
|
theme: SnapshotTheme(
|
||||||
textColorHex: defaults.string(forKey: "textColor") ?? existing.textColorHex,
|
today: defaults.string(forKey: "todayColor") ?? existing.theme.today,
|
||||||
backgroundColorHex: defaults.string(forKey: "backgroundColor") ?? existing.backgroundColorHex,
|
text: defaults.string(forKey: "textColor") ?? existing.theme.text,
|
||||||
lineColorHex: defaults.string(forKey: "lineColor") ?? existing.lineColorHex,
|
background: defaults.string(forKey: "backgroundColor") ?? existing.theme.background,
|
||||||
primaryColorHex: defaults.string(forKey: "primaryColor") ?? existing.primaryColorHex,
|
line: defaults.string(forKey: "lineColor") ?? existing.theme.line,
|
||||||
accentColorHex: defaults.string(forKey: "accentColor") ?? existing.accentColorHex,
|
primary: defaults.string(forKey: "primaryColor") ?? existing.theme.primary,
|
||||||
language: defaults.string(forKey: "appLanguage") ?? existing.language
|
accent: defaults.string(forKey: "accentColor") ?? existing.theme.accent),
|
||||||
)
|
language: defaults.string(forKey: "appLanguage") ?? existing.language)
|
||||||
write(updated)
|
write(updated)
|
||||||
WidgetTimelineNotifier.reload()
|
WidgetTimelineNotifier.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a snapshot from the app's current state. Kept here so the coverage
|
||||||
|
/// window and the writer version are stamped in exactly one place.
|
||||||
|
static func makeSnapshot(events: [WidgetEvent],
|
||||||
|
coverageStart: Date,
|
||||||
|
coverageEnd: Date) -> WidgetSnapshot {
|
||||||
|
let defaults = UserDefaults.standard
|
||||||
|
return WidgetSnapshot(
|
||||||
|
writtenAt: Date(),
|
||||||
|
coverageStart: coverageStart,
|
||||||
|
coverageEnd: coverageEnd,
|
||||||
|
isLoggedIn: true, // only ever written while signed in
|
||||||
|
writerVersion: writerVersion,
|
||||||
|
events: events,
|
||||||
|
theme: SnapshotTheme(
|
||||||
|
today: defaults.string(forKey: "todayColor") ?? "#4285f4",
|
||||||
|
text: defaults.string(forKey: "textColor") ?? "#FFFFFF",
|
||||||
|
background: defaults.string(forKey: "backgroundColor") ?? "#000000",
|
||||||
|
line: defaults.string(forKey: "lineColor") ?? "#3A3A52",
|
||||||
|
primary: defaults.string(forKey: "primaryColor") ?? "#4285f4",
|
||||||
|
accent: defaults.string(forKey: "accentColor") ?? "#ea4335"),
|
||||||
|
language: defaults.string(forKey: "appLanguage") ?? "system")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WidgetTimelineNotifier {
|
enum WidgetTimelineNotifier {
|
||||||
|
|||||||
Reference in New Issue
Block a user