Archived
Compare commits
12
Commits
6a7afe0647
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d7a0080e | ||
|
|
e5b3e549e8 | ||
|
|
465c465af2 | ||
|
|
6e91eaaaa2 | ||
|
|
1a1230a796 | ||
|
|
2c26be2ab5 | ||
|
|
938ca7ca98 | ||
|
|
b489f10c28 | ||
|
|
fdae680e02 | ||
|
|
e7fbe5c092 | ||
|
|
d0cff20be8 | ||
|
|
f9bc1965a5 |
@@ -10,13 +10,13 @@
|
|||||||
3934CF4630544C2E003958B8 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3934CF4530544C2E003958B8 /* WidgetKit.framework */; };
|
3934CF4630544C2E003958B8 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3934CF4530544C2E003958B8 /* WidgetKit.framework */; };
|
||||||
3934CF4830544C2E003958B8 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3934CF4730544C2E003958B8 /* SwiftUI.framework */; };
|
3934CF4830544C2E003958B8 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3934CF4730544C2E003958B8 /* SwiftUI.framework */; };
|
||||||
3934CF5530544C2E003958B8 /* CalendarrWatchComplications.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3934CF4330544C2E003958B8 /* CalendarrWatchComplications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
3934CF5530544C2E003958B8 /* CalendarrWatchComplications.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3934CF4330544C2E003958B8 /* CalendarrWatchComplications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
39E1B0010001000000000001 /* CalendarrWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 3934CF1A30544C01003958B8 /* CalendarrWatch.app */; platformFilters = (ios, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
AA9701000001000000000001 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA9701000002000000000001 /* AppAuth */; };
|
AA9701000001000000000001 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA9701000002000000000001 /* AppAuth */; };
|
||||||
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000001 /* CalendarrCore */; };
|
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000001 /* CalendarrCore */; };
|
||||||
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000002 /* CalendarrCore */; };
|
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000002 /* CalendarrCore */; };
|
||||||
CA1E4D0A0002000000000003 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000003 /* CalendarrCore */; };
|
CA1E4D0A0002000000000003 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000003 /* CalendarrCore */; };
|
||||||
CA1E4D0A0002000000000004 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000004 /* CalendarrCore */; };
|
CA1E4D0A0002000000000004 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000004 /* CalendarrCore */; };
|
||||||
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, ); }; };
|
||||||
39E1B0010001000000000001 /* CalendarrWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 3934CF1A30544C01003958B8 /* CalendarrWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -48,13 +48,6 @@
|
|||||||
remoteGlobalIDString = 3934CF4230544C2E003958B8;
|
remoteGlobalIDString = 3934CF4230544C2E003958B8;
|
||||||
remoteInfo = CalendarrWatchComplications;
|
remoteInfo = CalendarrWatchComplications;
|
||||||
};
|
};
|
||||||
D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = D0001A01ABCDEF0100AB5001;
|
|
||||||
remoteInfo = CalendarrWidgets;
|
|
||||||
};
|
|
||||||
39E1B0010003000000000001 /* PBXContainerItemProxy */ = {
|
39E1B0010003000000000001 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
@@ -62,6 +55,13 @@
|
|||||||
remoteGlobalIDString = 3934CF1930544C01003958B8;
|
remoteGlobalIDString = 3934CF1930544C01003958B8;
|
||||||
remoteInfo = CalendarrWatch;
|
remoteInfo = CalendarrWatch;
|
||||||
};
|
};
|
||||||
|
D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = D0001A01ABCDEF0100AB5001;
|
||||||
|
remoteInfo = CalendarrWidgets;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
@@ -76,17 +76,6 @@
|
|||||||
name = "Embed Foundation Extensions";
|
name = "Embed Foundation Extensions";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
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;
|
|
||||||
};
|
|
||||||
39E1B0010002000000000001 /* Embed Watch Content */ = {
|
39E1B0010002000000000001 /* Embed Watch Content */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -98,6 +87,17 @@
|
|||||||
name = "Embed Watch Content";
|
name = "Embed Watch Content";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
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 */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -158,6 +158,11 @@
|
|||||||
path = CalendarrWatchComplications;
|
path = CalendarrWatchComplications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
path = CalendarrWatchShared;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C0000D01FC4E10100AB5001 /* Calendarr iOS */ = {
|
C0000D01FC4E10100AB5001 /* Calendarr iOS */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = "Calendarr iOS";
|
path = "Calendarr iOS";
|
||||||
@@ -260,6 +265,7 @@
|
|||||||
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */,
|
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */,
|
||||||
3934CF4430544C2E003958B8 /* Frameworks */,
|
3934CF4430544C2E003958B8 /* Frameworks */,
|
||||||
C0000C01FB4E10100AB5001 /* Products */,
|
C0000C01FB4E10100AB5001 /* Products */,
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -319,6 +325,7 @@
|
|||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
3934CF1B30544C01003958B8 /* CalendarrWatch */,
|
3934CF1B30544C01003958B8 /* CalendarrWatch */,
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */,
|
||||||
);
|
);
|
||||||
name = CalendarrWatch;
|
name = CalendarrWatch;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -388,6 +395,7 @@
|
|||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */,
|
3934CF4930544C2E003958B8 /* CalendarrWatchComplications */,
|
||||||
|
39E1B0010005000000000001 /* CalendarrWatchShared */,
|
||||||
);
|
);
|
||||||
name = CalendarrWatchComplications;
|
name = CalendarrWatchComplications;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -641,16 +649,19 @@
|
|||||||
target = 3934CF4230544C2E003958B8 /* CalendarrWatchComplications */;
|
target = 3934CF4230544C2E003958B8 /* CalendarrWatchComplications */;
|
||||||
targetProxy = 3934CF5330544C2E003958B8 /* PBXContainerItemProxy */;
|
targetProxy = 3934CF5330544C2E003958B8 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
39E1B0010004000000000001 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
platformFilters = (
|
||||||
|
ios,
|
||||||
|
);
|
||||||
|
target = 3934CF1930544C01003958B8 /* CalendarrWatch */;
|
||||||
|
targetProxy = 39E1B0010003000000000001 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */ = {
|
D0001A0EABCDEF0100AB5001 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */;
|
target = D0001A01ABCDEF0100AB5001 /* CalendarrWidgets */;
|
||||||
targetProxy = D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */;
|
targetProxy = D0001A0DABCDEF0100AB5001 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
39E1B0010004000000000001 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 3934CF1930544C01003958B8 /* CalendarrWatch */;
|
|
||||||
targetProxy = 39E1B0010003000000000001 /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@@ -709,20 +720,20 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
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 = CalendarrWatch/CalendarrWatch.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = CalendarrWatch;
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.scarriffleservices.calendarr.ios;
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.scarriffleservices.calendarr.ios;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
CODE_SIGN_ENTITLEMENTS = CalendarrWatch/CalendarrWatch.entitlements;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -743,20 +754,20 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
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 = CalendarrWatch/CalendarrWatch.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = CalendarrWatch;
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.scarriffleservices.calendarr.ios;
|
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.scarriffleservices.calendarr.ios;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
CODE_SIGN_ENTITLEMENTS = CalendarrWatch/CalendarrWatch.entitlements;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -777,10 +788,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppTests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppTests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -800,10 +811,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppTests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppTests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -822,10 +833,10 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppUITests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppUITests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -844,10 +855,10 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppUITests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.scarriffleservices.calendarr.ios.watchkitapp.CalendarrWatch-Watch-AppUITests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -867,12 +878,13 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = CalendarrWatchComplications/CalendarrWatchComplications.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = CalendarrWatchComplications/Info.plist;
|
INFOPLIST_FILE = CalendarrWatchComplications/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = CalendarrWatch;
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -880,8 +892,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
"@executable_path/../../../../Frameworks",
|
"@executable_path/../../../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
CODE_SIGN_ENTITLEMENTS = CalendarrWatchComplications/CalendarrWatchComplications.entitlements;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp.complications;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp.complications;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -901,12 +912,13 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = CalendarrWatchComplications/CalendarrWatchComplications.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = CalendarrWatchComplications/Info.plist;
|
INFOPLIST_FILE = CalendarrWatchComplications/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = CalendarrWatch;
|
INFOPLIST_KEY_CFBundleDisplayName = Calendarr;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -914,8 +926,7 @@
|
|||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
"@executable_path/../../../../Frameworks",
|
"@executable_path/../../../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 3.5;
|
||||||
CODE_SIGN_ENTITLEMENTS = CalendarrWatchComplications/CalendarrWatchComplications.entitlements;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp.complications;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios.watchkitapp.complications;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
@@ -1079,11 +1090,13 @@
|
|||||||
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 = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
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.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -1100,7 +1113,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
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,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -1129,11 +1142,13 @@
|
|||||||
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 = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
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.6;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"-ObjC",
|
"-ObjC",
|
||||||
@@ -1150,7 +1165,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
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,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -1185,7 +1200,7 @@
|
|||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -1219,7 +1234,7 @@
|
|||||||
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,2";
|
||||||
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
"TARGETED_DEVICE_FAMILY[sdk=macosx*]" = 2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -1329,6 +1344,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = CalendarrCore;
|
productName = CalendarrCore;
|
||||||
};
|
};
|
||||||
|
CA1E4D0A0001000000000002 /* CalendarrCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = CalendarrCore;
|
||||||
|
};
|
||||||
CA1E4D0A0001000000000003 /* CalendarrCore */ = {
|
CA1E4D0A0001000000000003 /* CalendarrCore */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = CalendarrCore;
|
productName = CalendarrCore;
|
||||||
@@ -1337,10 +1356,6 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = CalendarrCore;
|
productName = CalendarrCore;
|
||||||
};
|
};
|
||||||
CA1E4D0A0001000000000002 /* CalendarrCore */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = CalendarrCore;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = C0000301FB4E10100AB5001 /* Project object */;
|
rootObject = C0000301FB4E10100AB5001 /* Project object */;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF2530544C02003958B8"
|
||||||
|
BuildableName = "CalendarrWatchTests.xctest"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF2F30544C02003958B8"
|
||||||
|
BuildableName = "CalendarrWatchUITests.xctest"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF4230544C2E003958B8"
|
||||||
|
BuildableName = "CalendarrWatchComplications.appex"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF1930544C01003958B8"
|
||||||
|
BuildableName = "CalendarrWatch.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetKind"
|
||||||
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3934CF4230544C2E003958B8"
|
||||||
|
BuildableName = "CalendarrWatchComplications.appex"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
wasCreatedForAppExtension = "YES"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "D0001A01ABCDEF0100AB5001"
|
||||||
|
BuildableName = "CalendarrWidgets.appex"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<EnvironmentVariables>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetKind"
|
||||||
|
value = ""
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetDefaultView"
|
||||||
|
value = "timeline"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "_XCWidgetFamily"
|
||||||
|
value = "systemMedium"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
|
</EnvironmentVariables>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C0000A01FB4E10100AB5001"
|
||||||
|
BuildableName = "Calendarr iOS.app"
|
||||||
|
ReferencedContainer = "container:Calendarr iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -91,11 +91,32 @@ struct CalEvent: Identifiable, Hashable {
|
|||||||
else if let n = json["id"] as? Int { id = String(n) }
|
else if let n = json["id"] as? Int { id = String(n) }
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
let isAllDay = json["allDay"] as? Bool ?? false
|
// A date-only string is an all-day event whether or not the flag says
|
||||||
let startDate = parseDate(startStr, allDay: isAllDay)
|
// so — `parseDate` already treats it that way. Trusting the flag alone
|
||||||
let endDate = parseDate(endStr, allDay: isAllDay)
|
// is what put "00:00" in front of birthdays everywhere they are shown.
|
||||||
|
let startIsDateOnly: Bool = {
|
||||||
|
let clean = startStr.trimmingCharacters(in: .whitespaces)
|
||||||
|
return clean.count == 10 && !clean.contains("T")
|
||||||
|
}()
|
||||||
|
let declaredAllDay = (json["allDay"] as? Bool ?? false) || startIsDateOnly
|
||||||
|
let startDate = parseDate(startStr, allDay: declaredAllDay)
|
||||||
|
let endDate = parseDate(endStr, allDay: declaredAllDay)
|
||||||
guard let s = startDate, let e = endDate else { return nil }
|
guard let s = startDate, let e = endDate else { return nil }
|
||||||
|
|
||||||
|
// An event running midnight to midnight across one or more whole days
|
||||||
|
// is all-day whatever the payload claims. Without this, a six-day
|
||||||
|
// holiday sent without the flag renders as "00:00 – 00:00" — a time
|
||||||
|
// range that is technically true and tells the reader nothing.
|
||||||
|
let cal = Calendar.current
|
||||||
|
// Requiring the end to land exactly on midnight too was too strict: a
|
||||||
|
// holiday stored as 16th 00:00 to 21st 01:00 slipped through and showed
|
||||||
|
// up as a one-hour slot. Starting on a day boundary and running into a
|
||||||
|
// later day is enough.
|
||||||
|
let spansWholeDays = e > s
|
||||||
|
&& cal.startOfDay(for: s) == s
|
||||||
|
&& cal.startOfDay(for: e) > cal.startOfDay(for: s)
|
||||||
|
let isAllDay = declaredAllDay || spansWholeDays
|
||||||
|
|
||||||
return CalEvent(
|
return CalEvent(
|
||||||
id: id,
|
id: id,
|
||||||
url: json["url"] as? String ?? "",
|
url: json["url"] as? String ?? "",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
|
"filename" : "AppIcon-1024.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "watchos",
|
"platform" : "watchos",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
//
|
|
||||||
// CalendarrWatchApp.swift
|
|
||||||
// CalendarrWatch Watch App
|
|
||||||
//
|
|
||||||
// Created by Guido Schmit on 11.09.2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct CalendarrWatch_Watch_AppApp: App {
|
struct CalendarrWatchApp: App {
|
||||||
|
@State private var model = WatchSnapshotModel()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// The session has to be live before the first payload arrives, and a
|
||||||
|
// complication push can wake this app with no UI on screen at all.
|
||||||
|
WatchSnapshotReceiver.shared.activate()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
WatchAgendaView()
|
||||||
|
.environment(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// CalendarrWatch Watch App
|
|
||||||
//
|
|
||||||
// Created by Guido Schmit on 11.09.2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
.imageScale(.large)
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
Text("Hello, world!")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// The whole app: a scrolling agenda grouped by day, plus a way to reach the
|
||||||
|
/// calendar filter. Read-only by design — the snapshot carries neither
|
||||||
|
/// recurrence rules nor permissions, so it is deliberately not enough to edit.
|
||||||
|
///
|
||||||
|
/// Deliberately does **not** apply `snapshot.theme` to the background. The
|
||||||
|
/// always-on display and OLED make black the right choice here; the theme only
|
||||||
|
/// contributes the per-event accent colours.
|
||||||
|
struct WatchAgendaView: View {
|
||||||
|
@Environment(WatchSnapshotModel.self) private var model
|
||||||
|
@State private var showingFilter = false
|
||||||
|
|
||||||
|
private var calendar: Calendar {
|
||||||
|
var calendar = Calendar(identifier: .gregorian)
|
||||||
|
calendar.locale = WatchL10n.locale(model.language)
|
||||||
|
return calendar
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
content
|
||||||
|
.navigationTitle(WatchL10n.t("watch.title", model.language))
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button { showingFilter = true } label: {
|
||||||
|
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||||
|
}
|
||||||
|
.accessibilityLabel(WatchL10n.t("watch.calendars", model.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingFilter) { WatchCalendarFilterView() }
|
||||||
|
}
|
||||||
|
.onAppear { WatchSnapshotReceiver.shared.requestRefresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
if let snapshot = model.visible, !groups(in: snapshot).isEmpty {
|
||||||
|
List {
|
||||||
|
ForEach(groups(in: snapshot), id: \.day) { group in
|
||||||
|
Section(WatchEventFormat.dayHeader(group.day, language: model.language,
|
||||||
|
calendar: calendar)) {
|
||||||
|
ForEach(group.events) { event in
|
||||||
|
NavigationLink {
|
||||||
|
WatchEventDetailView(event: event,
|
||||||
|
calendarName: model.calendarName(for: event.calendarKey),
|
||||||
|
language: model.language)
|
||||||
|
} label: {
|
||||||
|
WatchEventRow(event: event, language: model.language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("\(WatchL10n.t("watch.as_of", model.language)) \(WatchEventFormat.time(snapshot.writtenAt, language: model.language))")
|
||||||
|
// Beyond the coverage window the snapshot carries no
|
||||||
|
// information, which is not the same as "nothing
|
||||||
|
// scheduled". Saying so is what keeps the empty tail of
|
||||||
|
// the list from reading as a free afternoon.
|
||||||
|
Text("\(WatchL10n.t("watch.no_information", model.language)) \(coverageEndLine(snapshot))")
|
||||||
|
if let error = WatchSnapshotReceiver.shared.lastError {
|
||||||
|
Text(error).foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.carousel)
|
||||||
|
} else {
|
||||||
|
WatchStatusView(result: model.result, language: model.language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct DayGroup {
|
||||||
|
let day: Date
|
||||||
|
let events: [SnapshotEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last day the snapshot can speak for, formatted for the footer.
|
||||||
|
private func coverageEndLine(_ snapshot: CalendarrSnapshot) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(model.language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("d MMM")
|
||||||
|
let lastCovered = calendar.date(byAdding: .day, value: -1, to: snapshot.coverageEnd)
|
||||||
|
return formatter.string(from: lastCovered ?? snapshot.coverageEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Group the covered window by day. Days the snapshot cannot speak for are
|
||||||
|
/// simply absent — never rendered as "nothing scheduled", which would look
|
||||||
|
/// correct and be false.
|
||||||
|
private func groups(in snapshot: CalendarrSnapshot) -> [DayGroup] {
|
||||||
|
let calendar = self.calendar
|
||||||
|
let from = max(snapshot.coverageStart, calendar.startOfDay(for: Date()))
|
||||||
|
var buckets: [Date: [SnapshotEvent]] = [:]
|
||||||
|
|
||||||
|
for event in snapshot.events where event.end > from {
|
||||||
|
var day = calendar.startOfDay(for: max(event.start, from))
|
||||||
|
// A multi-day event appears on each day it covers.
|
||||||
|
while day < event.end && day < snapshot.coverageEnd {
|
||||||
|
buckets[day, default: []].append(event)
|
||||||
|
guard let next = calendar.date(byAdding: .day, value: 1, to: day) else { break }
|
||||||
|
day = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buckets
|
||||||
|
.map { DayGroup(day: $0.key, events: $0.value.sorted { $0.start < $1.start }) }
|
||||||
|
.sorted { $0.day < $1.day }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Narrows what this watch shows, on top of what the phone already filtered.
|
||||||
|
/// One toggle here affects the agenda and every complication at once.
|
||||||
|
struct WatchCalendarFilterView: View {
|
||||||
|
@Environment(WatchSnapshotModel.self) private var model
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
if model.calendars.isEmpty {
|
||||||
|
Text(WatchL10n.t("watch.waiting", model.language))
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
ForEach(model.calendars) { calendar in
|
||||||
|
row(calendar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(WatchL10n.t("watch.calendars", model.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func row(_ calendar: SnapshotCalendar) -> some View {
|
||||||
|
let shown = !model.isHidden(calendar.id)
|
||||||
|
return Button {
|
||||||
|
model.setHidden(calendar.id, shown)
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
// A checkbox, not a Toggle: a switch reads as a state indicator
|
||||||
|
// you wait on rather than a control you tap.
|
||||||
|
Image(systemName: shown ? "checkmark.square.fill" : "square")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundStyle(shown ? Color.accentColor : Color.secondary)
|
||||||
|
Circle()
|
||||||
|
.fill(Color(watchHex: calendar.colorHex))
|
||||||
|
.frame(width: 7, height: 7)
|
||||||
|
Text(calendar.name)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityAddTraits(shown ? [.isSelected] : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Everything the snapshot carries about one event, which is deliberately not
|
||||||
|
/// everything the server knows: notes, recurrence and permissions are dropped
|
||||||
|
/// on the way out, so there is nothing here to edit.
|
||||||
|
struct WatchEventDetailView: View {
|
||||||
|
let event: SnapshotEvent
|
||||||
|
/// Nil when the calendar list has not arrived yet, which is possible right
|
||||||
|
/// after a first install.
|
||||||
|
let calendarName: String?
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .top, spacing: 6) {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(watchHex: event.colorHex))
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
.padding(.top, 5)
|
||||||
|
Text(event.title)
|
||||||
|
.font(.headline)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label(WatchEventFormat.timeRange(event, language: language),
|
||||||
|
systemImage: "clock")
|
||||||
|
.font(.footnote)
|
||||||
|
|
||||||
|
Label(dateLine, systemImage: "calendar")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if !event.location.isEmpty {
|
||||||
|
Label(event.location, systemImage: "mappin.and.ellipse")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let calendarName {
|
||||||
|
Label(calendarName, systemImage: "list.bullet.rectangle")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dateLine: String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("EEEE d MMMM")
|
||||||
|
return formatter.string(from: event.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// One event: a colour bar for the calendar, the time range, the title.
|
||||||
|
struct WatchEventRow: View {
|
||||||
|
let event: SnapshotEvent
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 7) {
|
||||||
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.fill(Color(watchHex: event.colorHex))
|
||||||
|
.frame(width: 3)
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text(WatchEventFormat.timeRange(event, language: language))
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(event.title)
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
/// Local copy of the hex initialiser: `CalendarrCore` is Foundation-only by
|
||||||
|
/// design, so colour parsing cannot live there.
|
||||||
|
init(watchHex hex: String) {
|
||||||
|
let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||||
|
var value: UInt64 = 0
|
||||||
|
Scanner(string: cleaned).scanHexInt64(&value)
|
||||||
|
guard cleaned.count == 6 else { self = .gray; return }
|
||||||
|
self.init(red: Double((value >> 16) & 0xFF) / 255,
|
||||||
|
green: Double((value >> 8) & 0xFF) / 255,
|
||||||
|
blue: Double(value & 0xFF) / 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WatchEventFormat {
|
||||||
|
static func timeRange(_ event: SnapshotEvent, language: String,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> String {
|
||||||
|
if event.isAllDay { return WatchL10n.t("watch.allday", language) }
|
||||||
|
|
||||||
|
let time = DateFormatter()
|
||||||
|
time.locale = WatchL10n.locale(language)
|
||||||
|
time.dateFormat = "HH:mm"
|
||||||
|
|
||||||
|
let start = time.string(from: event.start)
|
||||||
|
guard event.end > event.start else { return start }
|
||||||
|
|
||||||
|
// Without the weekday, an event running from Wednesday to Monday reads
|
||||||
|
// as a one-hour slot: "00:00 – 01:00" is true and useless.
|
||||||
|
if calendar.startOfDay(for: event.end) > calendar.startOfDay(for: event.start) {
|
||||||
|
let day = DateFormatter()
|
||||||
|
day.locale = WatchL10n.locale(language)
|
||||||
|
day.setLocalizedDateFormatFromTemplate("EEE")
|
||||||
|
return "\(day.string(from: event.start)) \(start) – "
|
||||||
|
+ "\(day.string(from: event.end)) \(time.string(from: event.end))"
|
||||||
|
}
|
||||||
|
return "\(start) – \(time.string(from: event.end))"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func time(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "HH:mm"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "Heute", "Morgen", else "Do 11. Sep".
|
||||||
|
static func dayHeader(_ day: Date, language: String,
|
||||||
|
calendar: Calendar, now: Date = Date()) -> String {
|
||||||
|
if calendar.isDate(day, inSameDayAs: now) {
|
||||||
|
return WatchL10n.t("watch.today", language)
|
||||||
|
}
|
||||||
|
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: now)),
|
||||||
|
calendar.isDate(day, inSameDayAs: tomorrow) {
|
||||||
|
return WatchL10n.t("watch.tomorrow", language)
|
||||||
|
}
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("EEE d MMM")
|
||||||
|
return formatter.string(from: day)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Every reason there is nothing to show, named. "Never received", "signed out"
|
||||||
|
/// and "genuinely empty" call for different sentences, and showing the wrong one
|
||||||
|
/// sends the user looking in the wrong place.
|
||||||
|
struct WatchStatusView: View {
|
||||||
|
let result: SnapshotReadResult
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image(systemName: symbol)
|
||||||
|
.font(.system(size: 22))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(message)
|
||||||
|
.font(.footnote)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var symbol: String {
|
||||||
|
switch result {
|
||||||
|
case .ok: return "calendar"
|
||||||
|
case .loggedOut: return "person.crop.circle.badge.xmark"
|
||||||
|
case .neverWritten: return "iphone.and.arrow.forward"
|
||||||
|
case .incompatible: return "arrow.down.circle"
|
||||||
|
case .unreadable: return "exclamationmark.triangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var message: String {
|
||||||
|
switch result {
|
||||||
|
case .ok: return WatchL10n.t("watch.no_events", language)
|
||||||
|
case .loggedOut: return WatchL10n.t("watch.sign_in", language)
|
||||||
|
case .neverWritten: return WatchL10n.t("watch.waiting", language)
|
||||||
|
case .incompatible: return WatchL10n.t("watch.update", language)
|
||||||
|
case .unreadable: return WatchL10n.t("watch.unreadable", language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// What the watch views read. Holds the last received snapshot, the calendar
|
||||||
|
/// list, and this watch's filter.
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class WatchSnapshotModel {
|
||||||
|
private(set) var result: SnapshotReadResult = .neverWritten
|
||||||
|
private(set) var calendars: [SnapshotCalendar] = []
|
||||||
|
private(set) var hiddenKeys: Set<String> = WatchFilterStore.load()
|
||||||
|
|
||||||
|
private let store = SnapshotStore()
|
||||||
|
private var changeToken: SnapshotChangeNotifier.ObservationToken?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
reload()
|
||||||
|
// The receiver writes through SnapshotStore, which broadcasts. Listening
|
||||||
|
// here means the UI does not have to be notified separately.
|
||||||
|
changeToken = SnapshotChangeNotifier.observe { [weak self] in
|
||||||
|
Task { @MainActor in self?.reload() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reload() {
|
||||||
|
result = store.read()
|
||||||
|
calendars = store.readCalendars()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The snapshot with this watch's filter applied, or nil when there is
|
||||||
|
/// nothing usable to show.
|
||||||
|
var visible: CalendarrSnapshot? {
|
||||||
|
result.snapshot?.filtered(excludingCalendarKeys: hiddenKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
var language: String { result.snapshot?.language ?? "system" }
|
||||||
|
|
||||||
|
func isHidden(_ key: String) -> Bool { hiddenKeys.contains(key) }
|
||||||
|
|
||||||
|
/// The display name for an event's calendar. Events carry only a
|
||||||
|
/// `calendarKey`; the names travel separately, in the calendar list.
|
||||||
|
func calendarName(for key: String) -> String? {
|
||||||
|
calendars.first { $0.id == key }?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHidden(_ key: String, _ hidden: Bool) {
|
||||||
|
if hidden { hiddenKeys.insert(key) } else { hiddenKeys.remove(key) }
|
||||||
|
WatchFilterStore.save(hiddenKeys)
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import Foundation
|
||||||
|
import WatchConnectivity
|
||||||
|
import WidgetKit
|
||||||
|
@_spi(Writer) import CalendarrCore
|
||||||
|
|
||||||
|
/// Takes what the phone sends and writes it into this watch's own App Group
|
||||||
|
/// container, using the same `SnapshotStore` the phone writes with. One wire
|
||||||
|
/// format, one reader, no drift.
|
||||||
|
///
|
||||||
|
/// Only the app runs a `WCSession`; a widget extension cannot. The complications
|
||||||
|
/// always read what this wrote.
|
||||||
|
@MainActor
|
||||||
|
final class WatchSnapshotReceiver: NSObject {
|
||||||
|
static let shared = WatchSnapshotReceiver()
|
||||||
|
|
||||||
|
private let store = SnapshotStore()
|
||||||
|
private let sessions = SharedSessionStore()
|
||||||
|
|
||||||
|
/// Shown in the agenda footer, because a silent failure here looks exactly
|
||||||
|
/// like an empty calendar.
|
||||||
|
private(set) var lastError: String?
|
||||||
|
|
||||||
|
func activate() {
|
||||||
|
guard WCSession.isSupported() else { return }
|
||||||
|
WCSession.default.delegate = self
|
||||||
|
WCSession.default.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the phone for the current state. Covers "the app is open in front of
|
||||||
|
/// me and looks stale"; the other two paths are push-driven.
|
||||||
|
func requestRefresh() {
|
||||||
|
let session = WCSession.default
|
||||||
|
guard session.activationState == .activated, session.isReachable else { return }
|
||||||
|
|
||||||
|
session.sendMessage(["request": "snapshot"],
|
||||||
|
replyHandler: { reply in
|
||||||
|
Task { @MainActor in self.apply(reply) }
|
||||||
|
},
|
||||||
|
errorHandler: { error in
|
||||||
|
Task { @MainActor in self.lastError = error.localizedDescription }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(_ dict: [String: Any]) {
|
||||||
|
guard !dict.isEmpty else { return }
|
||||||
|
do {
|
||||||
|
let payload = try WatchTransportPayload.decode(dict)
|
||||||
|
// This write posts SnapshotChangeNotifier, which is what the read
|
||||||
|
// model listens for — so the UI updates without being told twice.
|
||||||
|
try store.write(payload.snapshot)
|
||||||
|
try store.writeCalendars(payload.calendars)
|
||||||
|
if let session = payload.session { try sessions.write(session) }
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
lastError = nil
|
||||||
|
} catch {
|
||||||
|
lastError = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WatchSnapshotReceiver: WCSessionDelegate {
|
||||||
|
nonisolated func session(_ session: WCSession,
|
||||||
|
activationDidCompleteWith state: WCSessionActivationState,
|
||||||
|
error: Error?) {
|
||||||
|
if let error {
|
||||||
|
Task { @MainActor in self.lastError = error.localizedDescription }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main path: latest state only, delivered when this app launches or wakes.
|
||||||
|
nonisolated func session(_ session: WCSession,
|
||||||
|
didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||||
|
Task { @MainActor in self.apply(applicationContext) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The complication push, which is the only path that wakes this app while
|
||||||
|
/// it is not running.
|
||||||
|
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
||||||
|
Task { @MainActor in self.apply(userInfo) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
//
|
|
||||||
// CalendarrWatch.swift
|
|
||||||
// CalendarrWatch
|
|
||||||
//
|
|
||||||
// Created by Guido Schmit on 11.09.2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct Provider: TimelineProvider {
|
|
||||||
func placeholder(in context: Context) -> SimpleEntry {
|
|
||||||
SimpleEntry(date: Date(), emoji: "😀")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
|
||||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
|
||||||
completion(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
||||||
var entries: [SimpleEntry] = []
|
|
||||||
|
|
||||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
|
||||||
let currentDate = Date()
|
|
||||||
for hourOffset in 0 ..< 5 {
|
|
||||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
|
||||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
|
||||||
entries.append(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
|
||||||
completion(timeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// func relevances() async -> WidgetRelevances<Void> {
|
|
||||||
// // Generate a list containing the contexts this widget is relevant in.
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SimpleEntry: TimelineEntry {
|
|
||||||
let date: Date
|
|
||||||
let emoji: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CalendarrWatchEntryView : View {
|
|
||||||
var entry: Provider.Entry
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
Text("Time:")
|
|
||||||
Text(entry.date, style: .time)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("Emoji:")
|
|
||||||
Text(entry.emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CalendarrWatch: Widget {
|
|
||||||
let kind: String = "CalendarrWatch"
|
|
||||||
|
|
||||||
var body: some WidgetConfiguration {
|
|
||||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
|
||||||
if #available(watchOS 10.0, *) {
|
|
||||||
CalendarrWatchEntryView(entry: entry)
|
|
||||||
.containerBackground(.fill.tertiary, for: .widget)
|
|
||||||
} else {
|
|
||||||
CalendarrWatchEntryView(entry: entry)
|
|
||||||
.padding()
|
|
||||||
.background()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.configurationDisplayName("My Widget")
|
|
||||||
.description("This is an example widget.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview(as: .accessoryRectangular) {
|
|
||||||
CalendarrWatch()
|
|
||||||
} timeline: {
|
|
||||||
SimpleEntry(date: .now, emoji: "😀")
|
|
||||||
SimpleEntry(date: .now, emoji: "🤩")
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// CalendarrWatchBundle.swift
|
|
||||||
// CalendarrWatch
|
|
||||||
//
|
|
||||||
// Created by Guido Schmit on 11.09.2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
import WidgetKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct CalendarrWatchBundle: WidgetBundle {
|
|
||||||
var body: some Widget {
|
|
||||||
CalendarrWatch()
|
|
||||||
CalendarrWatchControl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct CalendarrWatchComplicationBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
WatchNextEventWidget()
|
||||||
|
WatchCornerCountdownWidget()
|
||||||
|
WatchCornerTitleWidget()
|
||||||
|
WatchCornerTimeWidget()
|
||||||
|
WatchCountdownWidget()
|
||||||
|
WatchDateWidget()
|
||||||
|
WatchTodayCountWidget()
|
||||||
|
WatchNextTwoWidget()
|
||||||
|
WatchDatePlusEventWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
/// Accessory families paint their own background; ours must stay out of
|
||||||
|
/// the way so the watch face's tint comes through.
|
||||||
|
func complicationChrome(_ language: String) -> some View {
|
||||||
|
containerBackground(for: .widget) { Color.clear }
|
||||||
|
.environment(\.locale, WatchL10n.locale(language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchNextEventWidget: Widget {
|
||||||
|
let kind = "WatchNextEvent"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
NextEventComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Nächster Termin")
|
||||||
|
.description("Zeit, Titel und Ort des nächsten Termins.")
|
||||||
|
.supportedFamilies([.accessoryRectangular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCornerCountdownWidget: Widget {
|
||||||
|
let kind = "WatchCornerCountdown"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CornerCountdownComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Restzeit (Ecke)")
|
||||||
|
.description("Restzeit bis zum nächsten Termin, Name am Rand.")
|
||||||
|
.supportedFamilies([.accessoryCorner])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCornerTitleWidget: Widget {
|
||||||
|
let kind = "WatchCornerTitle"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CornerTitleComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Terminname (Ecke)")
|
||||||
|
.description("Name des nächsten Termins, Uhrzeit klein am Rand.")
|
||||||
|
.supportedFamilies([.accessoryCorner])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCornerTimeWidget: Widget {
|
||||||
|
let kind = "WatchCornerTime"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CornerTimeComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Uhrzeit (Ecke)")
|
||||||
|
.description("Uhrzeit des nächsten Termins, Name am Rand.")
|
||||||
|
.supportedFamilies([.accessoryCorner])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchCountdownWidget: Widget {
|
||||||
|
let kind = "WatchCountdown"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
CountdownComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Countdown")
|
||||||
|
.description("Ring, der bis zum nächsten Termin leerläuft.")
|
||||||
|
.supportedFamilies([.accessoryCircular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchDateWidget: Widget {
|
||||||
|
let kind = "WatchDate"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
DateComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Datum")
|
||||||
|
.description("Tag und Monat.")
|
||||||
|
.supportedFamilies([.accessoryCircular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchTodayCountWidget: Widget {
|
||||||
|
let kind = "WatchTodayCount"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
TodayCountComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Termine heute")
|
||||||
|
.description("Anzahl der heutigen Termine.")
|
||||||
|
.supportedFamilies([.accessoryCircular, .accessoryInline])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchNextTwoWidget: Widget {
|
||||||
|
let kind = "WatchNextTwo"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
NextTwoComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Die nächsten zwei")
|
||||||
|
.description("Die beiden nächsten Termine.")
|
||||||
|
.supportedFamilies([.accessoryRectangular])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WatchDatePlusEventWidget: Widget {
|
||||||
|
let kind = "WatchDatePlusEvent"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
AppIntentConfiguration(kind: kind,
|
||||||
|
intent: WatchCalendarSelectionIntent.self,
|
||||||
|
provider: WatchComplicationProvider()) { entry in
|
||||||
|
DatePlusEventComplicationView(entry: entry).complicationChrome(entry.language)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Datum + Termin")
|
||||||
|
.description("Datum und nächster Termin in einem Slot.")
|
||||||
|
.supportedFamilies([.accessoryRectangular])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
//
|
|
||||||
// CalendarrWatchControl.swift
|
|
||||||
// CalendarrWatch
|
|
||||||
//
|
|
||||||
// Created by Guido Schmit on 11.09.2026.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppIntents
|
|
||||||
import SwiftUI
|
|
||||||
import WidgetKit
|
|
||||||
|
|
||||||
struct CalendarrWatchControl: ControlWidget {
|
|
||||||
var body: some ControlWidgetConfiguration {
|
|
||||||
StaticControlConfiguration(
|
|
||||||
kind: ".watchkitapp.CalendarrWatch",
|
|
||||||
provider: Provider()
|
|
||||||
) { value in
|
|
||||||
ControlWidgetToggle(
|
|
||||||
"Start Timer",
|
|
||||||
isOn: value,
|
|
||||||
action: StartTimerIntent()
|
|
||||||
) { isRunning in
|
|
||||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.displayName("Timer")
|
|
||||||
.description("A an example control that runs a timer.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CalendarrWatchControl {
|
|
||||||
struct Provider: ControlValueProvider {
|
|
||||||
var previewValue: Bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentValue() async throws -> Bool {
|
|
||||||
let isRunning = true // Check if the timer is running
|
|
||||||
return isRunning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StartTimerIntent: SetValueIntent {
|
|
||||||
static let title: LocalizedStringResource = "Start a timer"
|
|
||||||
|
|
||||||
@Parameter(title: "Timer is running")
|
|
||||||
var value: Bool
|
|
||||||
|
|
||||||
func perform() async throws -> some IntentResult {
|
|
||||||
// Start / stop the timer based on `value`.
|
|
||||||
return .result()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
// A corner holds **one** value plus the curved bezel label. A second inner line
|
||||||
|
// runs into the curved text — do not add one. `.widgetLabel` is what makes
|
||||||
|
// WidgetKit lay the text along the edge; there is no arc to draw by hand.
|
||||||
|
|
||||||
|
/// Value: live countdown. Label: time and title.
|
||||||
|
struct CornerCountdownComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
Group {
|
||||||
|
if let next {
|
||||||
|
Text(WatchComplicationSupport.compactCountdown(
|
||||||
|
to: WatchComplicationSupport.countdownTarget(next, at: entry.date),
|
||||||
|
at: entry.date,
|
||||||
|
language: entry.language))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
.widgetAccentable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widgetLabel {
|
||||||
|
Text(WatchComplicationSupport.eventLabel(next, at: entry.date, language: entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value: the appointment's clock time. Label: the title.
|
||||||
|
struct CornerTimeComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
Group {
|
||||||
|
if let next {
|
||||||
|
Text(next.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time(next.start, language: entry.language))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
.widgetAccentable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widgetLabel {
|
||||||
|
Text(next?.title ?? WatchL10n.t("watch.no_events", entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value: the appointment's name. Label: the time, small and curved.
|
||||||
|
///
|
||||||
|
/// This is the inverse of how the slot is shaped — the curved bezel is where a
|
||||||
|
/// corner has room for long text, and the inner area holds three or four
|
||||||
|
/// characters. The title therefore shrinks and truncates here. Built this way
|
||||||
|
/// on request; `CornerTimeComplicationView` is the same information with the
|
||||||
|
/// roles the other way round, so both can be compared on the face.
|
||||||
|
struct CornerTitleComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
Group {
|
||||||
|
if let next {
|
||||||
|
Text(next.title)
|
||||||
|
.font(.system(size: 13, weight: .medium))
|
||||||
|
.lineLimit(2)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.widgetAccentable()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widgetLabel {
|
||||||
|
Text(next.map {
|
||||||
|
$0.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time($0.start, language: entry.language)
|
||||||
|
} ?? WatchL10n.t("watch.no_events", entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Time, title, location. The variant that answers the actual question.
|
||||||
|
struct NextEventComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
inline(next)
|
||||||
|
default:
|
||||||
|
rectangular(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func rectangular(_ next: SnapshotEvent?) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
if let next {
|
||||||
|
Text(next.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time(next.start, language: entry.language))
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
Text(next.title)
|
||||||
|
.font(.system(size: 15))
|
||||||
|
.lineLimit(1)
|
||||||
|
if !next.location.isEmpty {
|
||||||
|
Text(next.location)
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
||||||
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func inline(_ next: SnapshotEvent?) -> some View {
|
||||||
|
let text: String = {
|
||||||
|
guard let next else { return WatchL10n.t("watch.no_events", entry.language) }
|
||||||
|
if next.isAllDay { return next.title }
|
||||||
|
return "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)"
|
||||||
|
}()
|
||||||
|
return Label(text, systemImage: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next two, so back-to-back appointments are visible at once.
|
||||||
|
struct NextTwoComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let events = WatchComplicationSupport.upcoming(in: entry.snapshot, at: entry.date, limit: 2)
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
if events.isEmpty {
|
||||||
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
||||||
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
||||||
|
} else {
|
||||||
|
ForEach(events) { event in
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Text(event.isAllDay
|
||||||
|
? WatchL10n.t("watch.allday", entry.language)
|
||||||
|
: WatchComplicationSupport.time(event.start, language: entry.language))
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
Text(event.title)
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weekday and date plus the next appointment, so one slot does both jobs.
|
||||||
|
struct DatePlusEventComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(dateLine)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
if let next {
|
||||||
|
Text(next.isAllDay
|
||||||
|
? next.title
|
||||||
|
: "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.lineLimit(1)
|
||||||
|
} else {
|
||||||
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dateLine: String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(entry.language)
|
||||||
|
formatter.setLocalizedDateFormatFromTemplate("EEE d MMMM")
|
||||||
|
return formatter.string(from: entry.date).uppercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Attaches the curved bezel label that a face with a bezel slot — Infograph —
|
||||||
|
/// renders across the top, where the tick marks otherwise are. Faces without
|
||||||
|
/// that slot ignore it, so it costs nothing to always provide.
|
||||||
|
private extension View {
|
||||||
|
func eventBezelLabel(_ entry: WatchComplicationEntry) -> some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
return widgetLabel {
|
||||||
|
Text(WatchComplicationSupport.eventLabel(next, at: entry.date, language: entry.language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A draining ring plus the remaining time.
|
||||||
|
struct CountdownComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
inline(next)
|
||||||
|
default:
|
||||||
|
circular(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func circular(_ next: SnapshotEvent?) -> some View {
|
||||||
|
if let next {
|
||||||
|
let target = WatchComplicationSupport.countdownTarget(next, at: entry.date)
|
||||||
|
Gauge(value: WatchComplicationSupport.gaugeFraction(to: target, at: entry.date)) {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
} currentValueLabel: {
|
||||||
|
Text(WatchComplicationSupport.compactCountdown(to: target, at: entry.date,
|
||||||
|
language: entry.language))
|
||||||
|
.font(.system(size: 13))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.widgetAccentable()
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
AccessoryWidgetBackground()
|
||||||
|
Image(systemName: "calendar").font(.system(size: 18))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func inline(_ next: SnapshotEvent?) -> some View {
|
||||||
|
if let next {
|
||||||
|
Label {
|
||||||
|
Text(WatchComplicationSupport.countdownTarget(next, at: entry.date), style: .timer)
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: "timer")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Label(WatchL10n.t("watch.no_events", entry.language), systemImage: "calendar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Weekday and day number, the way a date is actually read. The ring appears
|
||||||
|
/// only while an appointment is running, and then shows how far through it we
|
||||||
|
/// are — an empty track around an idle day is noise, so there is no Gauge at
|
||||||
|
/// all in that case.
|
||||||
|
struct DateComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
private var day: Int {
|
||||||
|
Calendar(identifier: .gregorian).component(.day, from: entry.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var weekday: String {
|
||||||
|
WatchComplicationSupport.weekdayAbbreviation(entry.date, language: entry.language)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
Label("\(weekday) \(day)", systemImage: "calendar")
|
||||||
|
default:
|
||||||
|
circular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var circular: some View {
|
||||||
|
if let running = WatchComplicationSupport.runningEvent(in: entry.snapshot, at: entry.date) {
|
||||||
|
Gauge(value: WatchComplicationSupport.progress(of: running, at: entry.date)) {
|
||||||
|
Text(weekday)
|
||||||
|
} currentValueLabel: {
|
||||||
|
dateStack
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
} else {
|
||||||
|
ZStack {
|
||||||
|
AccessoryWidgetBackground()
|
||||||
|
dateStack
|
||||||
|
}
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dateStack: some View {
|
||||||
|
VStack(spacing: -2) {
|
||||||
|
Text(weekday)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.widgetAccentable()
|
||||||
|
Text("\(day)")
|
||||||
|
.font(.system(size: 23, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many appointments today. Says "busy", not "what".
|
||||||
|
struct TodayCountComplicationView: View {
|
||||||
|
let entry: WatchComplicationEntry
|
||||||
|
@Environment(\.widgetFamily) private var family
|
||||||
|
|
||||||
|
private var count: Int {
|
||||||
|
WatchComplicationSupport.todayCount(in: entry.snapshot, at: entry.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch family {
|
||||||
|
case .accessoryInline:
|
||||||
|
Label("\(count) \(WatchL10n.t("watch.events_today", entry.language))",
|
||||||
|
systemImage: "calendar")
|
||||||
|
default:
|
||||||
|
Gauge(value: WatchComplicationSupport.todayRemainingFraction(in: entry.snapshot,
|
||||||
|
at: entry.date)) {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
} currentValueLabel: {
|
||||||
|
Text("\(count)")
|
||||||
|
.font(.system(size: 20, weight: .semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.6)
|
||||||
|
}
|
||||||
|
.gaugeStyle(.accessoryCircularCapacity)
|
||||||
|
.widgetAccentable()
|
||||||
|
.eventBezelLabel(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import AppIntents
|
||||||
|
import WidgetKit
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// One calendar option in a complication's configuration.
|
||||||
|
struct WatchCalendarEntity: AppEntity, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Kalender"
|
||||||
|
static var defaultQuery = WatchCalendarEntityQuery()
|
||||||
|
|
||||||
|
var displayRepresentation: DisplayRepresentation {
|
||||||
|
DisplayRepresentation(title: LocalizedStringResource(stringLiteral: name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the calendars from this watch's App Group container, so configuration
|
||||||
|
/// needs no network call and no phone.
|
||||||
|
struct WatchCalendarEntityQuery: EntityQuery {
|
||||||
|
func entities(for identifiers: [String]) async throws -> [WatchCalendarEntity] {
|
||||||
|
let wanted = Set(identifiers)
|
||||||
|
return SnapshotStore().readCalendars()
|
||||||
|
.filter { wanted.contains($0.id) }
|
||||||
|
.map { WatchCalendarEntity(id: $0.id, name: $0.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func suggestedEntities() async throws -> [WatchCalendarEntity] {
|
||||||
|
SnapshotStore().readCalendars().map { WatchCalendarEntity(id: $0.id, name: $0.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-complication override of the calendar selection. The in-app toggles are
|
||||||
|
/// the primary path — intent configuration on a watch face is awkward to
|
||||||
|
/// operate — so an empty selection means "whatever the app is set to".
|
||||||
|
struct WatchCalendarSelectionIntent: WidgetConfigurationIntent {
|
||||||
|
static var title: LocalizedStringResource = "Kalender auswählen"
|
||||||
|
static var description = IntentDescription("Leer = die Auswahl aus der Watch-App.")
|
||||||
|
|
||||||
|
// Must be optional: WidgetConfigurationIntent requires optional parameter
|
||||||
|
// types, and the watchOS SDK enforces it where iOS lets a bare array pass.
|
||||||
|
@Parameter(title: "Kalender")
|
||||||
|
var selectedCalendars: [WatchCalendarEntity]?
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import AppIntents
|
||||||
|
import SwiftUI
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
struct WatchComplicationEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
let snapshot: CalendarrSnapshot?
|
||||||
|
|
||||||
|
var language: String { snapshot?.language ?? "system" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds every Calendarr complication's timeline.
|
||||||
|
///
|
||||||
|
/// Entries land on event boundaries rather than on a fixed schedule, so the
|
||||||
|
/// complication flips in the exact minute the next appointment changes without
|
||||||
|
/// a network call and without spending refresh budget. New *data* arrives by
|
||||||
|
/// push from the phone, which is why the refresh policy is `.atEnd`: when the
|
||||||
|
/// timeline simply runs out, regenerating from the same snapshot is cheap and
|
||||||
|
/// correct.
|
||||||
|
struct WatchComplicationProvider: AppIntentTimelineProvider {
|
||||||
|
typealias Entry = WatchComplicationEntry
|
||||||
|
typealias Intent = WatchCalendarSelectionIntent
|
||||||
|
|
||||||
|
func placeholder(in context: Context) -> WatchComplicationEntry {
|
||||||
|
WatchComplicationEntry(date: .now, snapshot: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot(for configuration: WatchCalendarSelectionIntent,
|
||||||
|
in context: Context) async -> WatchComplicationEntry {
|
||||||
|
WatchComplicationEntry(date: .now, snapshot: resolved(configuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeline(for configuration: WatchCalendarSelectionIntent,
|
||||||
|
in context: Context) async -> Timeline<WatchComplicationEntry> {
|
||||||
|
let snapshot = resolved(configuration)
|
||||||
|
let now = Date()
|
||||||
|
let dates = snapshot.map { ComplicationTimeline.entryDates(from: now, in: $0) } ?? [now]
|
||||||
|
return Timeline(entries: dates.map { WatchComplicationEntry(date: $0, snapshot: snapshot) },
|
||||||
|
policy: .atEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the complication gallery offers before anyone configures anything.
|
||||||
|
/// Required on watchOS — iOS has a default implementation, the watch does
|
||||||
|
/// not, because the gallery has to be populated.
|
||||||
|
///
|
||||||
|
/// One entry, with nothing selected: that means "whatever the app is set
|
||||||
|
/// to", which is the filter the user already chose there.
|
||||||
|
func recommendations() -> [AppIntentRecommendation<WatchCalendarSelectionIntent>] {
|
||||||
|
[AppIntentRecommendation(intent: WatchCalendarSelectionIntent(),
|
||||||
|
description: Text("Calendarr"))]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app-level filter applies first; a per-complication selection narrows
|
||||||
|
/// further. An empty selection means "whatever the app is set to".
|
||||||
|
private func resolved(_ configuration: WatchCalendarSelectionIntent) -> CalendarrSnapshot? {
|
||||||
|
guard case .ok(let snapshot) = SnapshotStore().read() else { return nil }
|
||||||
|
return snapshot
|
||||||
|
.filtered(excludingCalendarKeys: WatchFilterStore.load())
|
||||||
|
.filtered(toCalendarKeys: Set((configuration.selectedCalendars ?? []).map(\.id)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import Foundation
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
enum WatchComplicationSupport {
|
||||||
|
/// The first event that has not ended yet — including one running now,
|
||||||
|
/// which is what "what is next" means when you are already in a meeting.
|
||||||
|
/// Matches `WatchPushPolicy.nextEventIdentity`, deliberately.
|
||||||
|
static func nextEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
||||||
|
snapshot?.events.filter { $0.end > now }.min { $0.start < $1.start }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func upcoming(in snapshot: CalendarrSnapshot?, at now: Date, limit: Int) -> [SnapshotEvent] {
|
||||||
|
guard let snapshot else { return [] }
|
||||||
|
return Array(snapshot.events.filter { $0.end > now }.sorted { $0.start < $1.start }.prefix(limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func todayCount(in snapshot: CalendarrSnapshot?, at now: Date,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Int {
|
||||||
|
guard let snapshot else { return 0 }
|
||||||
|
let dayStart = calendar.startOfDay(for: now)
|
||||||
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
||||||
|
return snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The appointment happening right now, if there is one. All-day events are
|
||||||
|
/// excluded on purpose: one of those is "running" for twenty-four hours and
|
||||||
|
/// would hold the ring at some arbitrary fraction all day, which says
|
||||||
|
/// nothing.
|
||||||
|
static func runningEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
||||||
|
snapshot?.events
|
||||||
|
.filter { !$0.isAllDay && $0.start <= now && $0.end > now }
|
||||||
|
.min { $0.end < $1.end }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How far through an appointment we are, 0 at its start and 1 at its end.
|
||||||
|
static func progress(of event: SnapshotEvent, at now: Date) -> Double {
|
||||||
|
let total = event.end.timeIntervalSince(event.start)
|
||||||
|
guard total > 0 else { return 0 }
|
||||||
|
return min(1, max(0, now.timeIntervalSince(event.start) / total))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func weekdayAbbreviation(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "EEE"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
.replacingOccurrences(of: ".", with: "")
|
||||||
|
.uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a countdown counts to: the start if it has not begun, otherwise the
|
||||||
|
/// end. Counting up from a start time that has passed reads as broken.
|
||||||
|
static func countdownTarget(_ event: SnapshotEvent, at now: Date) -> Date {
|
||||||
|
event.start > now ? event.start : event.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A countdown narrow enough for a corner or a circular gauge: at most
|
||||||
|
/// three characters. `Text(style: .timer)` would be live and free of
|
||||||
|
/// timeline entries, but renders `1:23:45` and overlaps whatever sits
|
||||||
|
/// beside it — so the timeline carries minute-granular entries instead and
|
||||||
|
/// this is recomputed per entry.
|
||||||
|
static func compactCountdown(to target: Date, at now: Date, language: String) -> String {
|
||||||
|
let seconds = target.timeIntervalSince(now)
|
||||||
|
guard seconds > 0 else { return WatchL10n.t("watch.now", language) }
|
||||||
|
let minutes = Int(seconds / 60)
|
||||||
|
if minutes < 1 { return WatchL10n.t("watch.now", language) }
|
||||||
|
if minutes < 60 { return "\(minutes)m" }
|
||||||
|
let hours = minutes / 60
|
||||||
|
if hours < 24 { return "\(hours)h" }
|
||||||
|
return "\(hours / 24)d"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How full a draining ring should be, over a two-hour lead window.
|
||||||
|
static func gaugeFraction(to target: Date, at now: Date) -> Double {
|
||||||
|
let window: TimeInterval = 2 * 3600
|
||||||
|
return min(1, max(0, target.timeIntervalSince(now) / window))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of today is still ahead. Drains through the day, matching the
|
||||||
|
/// countdown ring rather than inventing a second direction.
|
||||||
|
static func dayRemainingFraction(at now: Date,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
|
||||||
|
let dayStart = calendar.startOfDay(for: now)
|
||||||
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
||||||
|
let total = dayEnd.timeIntervalSince(dayStart)
|
||||||
|
guard total > 0 else { return 0 }
|
||||||
|
return min(1, max(0, dayEnd.timeIntervalSince(now) / total))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The share of today's appointments still ahead. Zero when there are none,
|
||||||
|
/// which leaves the ring empty rather than full — an empty day should not
|
||||||
|
/// look like an untouched one.
|
||||||
|
static func todayRemainingFraction(in snapshot: CalendarrSnapshot?, at now: Date,
|
||||||
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
|
||||||
|
guard let snapshot else { return 0 }
|
||||||
|
let dayStart = calendar.startOfDay(for: now)
|
||||||
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
||||||
|
let today = snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }
|
||||||
|
guard !today.isEmpty else { return 0 }
|
||||||
|
let ahead = today.filter { $0.end > now }.count
|
||||||
|
return min(1, max(0, Double(ahead) / Double(today.count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func time(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "HH:mm"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func monthAbbreviation(_ date: Date, language: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = WatchL10n.locale(language)
|
||||||
|
formatter.dateFormat = "LLL"
|
||||||
|
return formatter.string(from: date).uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The curved label that accompanies a circular or corner complication:
|
||||||
|
/// time plus title, or a plain "no events".
|
||||||
|
///
|
||||||
|
/// On a corner this follows the screen edge. On a face with a bezel slot —
|
||||||
|
/// Infograph — the same label arcs across the top, where the tick marks
|
||||||
|
/// otherwise are. That is the position ClockKit called `graphicBezel`, and
|
||||||
|
/// in WidgetKit it is reached by attaching `.widgetLabel` to an
|
||||||
|
/// `accessoryCircular` widget.
|
||||||
|
static func eventLabel(_ event: SnapshotEvent?, at now: Date, language: String) -> String {
|
||||||
|
guard let event else { return WatchL10n.t("watch.no_events", language) }
|
||||||
|
if event.isAllDay { return event.title }
|
||||||
|
return "\(time(event.start, language: language)) \(event.title)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Foundation
|
||||||
|
import CalendarrCore
|
||||||
|
|
||||||
|
/// Which calendars this watch hides, on top of what the phone already filtered.
|
||||||
|
///
|
||||||
|
/// The snapshot arrives with hidden and banished calendars already removed, so
|
||||||
|
/// this narrows further and can never contradict the phone. It lives in the App
|
||||||
|
/// Group so one toggle in the app affects every complication at once.
|
||||||
|
enum WatchFilterStore {
|
||||||
|
private static let key = "watchHiddenCalendarKeys"
|
||||||
|
|
||||||
|
private static var defaults: UserDefaults? {
|
||||||
|
UserDefaults(suiteName: CalendarrAppGroup.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func load() -> Set<String> {
|
||||||
|
Set(defaults?.stringArray(forKey: key) ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ keys: Set<String>) {
|
||||||
|
defaults?.set(keys.sorted(), forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// German and English strings for the watch app and its complications.
|
||||||
|
///
|
||||||
|
/// A local copy rather than a shared one: `CalendarrCore` is deliberately
|
||||||
|
/// Foundation-only and holds no presentation concerns, and the iOS `WidgetL10n`
|
||||||
|
/// belongs to a target the watch cannot link against.
|
||||||
|
enum WatchL10n {
|
||||||
|
static func t(_ key: String, _ stored: String) -> String {
|
||||||
|
strings[resolve(stored)]?[key] ?? strings["en"]?[key] ?? key
|
||||||
|
}
|
||||||
|
|
||||||
|
static func locale(_ stored: String) -> Locale {
|
||||||
|
Locale(identifier: resolve(stored) == "de" ? "de_DE" : "en_US")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func resolve(_ stored: String) -> String {
|
||||||
|
if stored == "de" || stored == "en" { return stored }
|
||||||
|
let preferred = Locale.preferredLanguages.first ?? "en"
|
||||||
|
return preferred.lowercased().hasPrefix("de") ? "de" : "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let strings: [String: [String: String]] = [
|
||||||
|
"de": [
|
||||||
|
"watch.title": "Calendarr",
|
||||||
|
"watch.calendars": "Kalender",
|
||||||
|
"watch.allday": "Ganztägig",
|
||||||
|
"watch.no_events": "Keine Termine",
|
||||||
|
"watch.now": "jetzt",
|
||||||
|
"watch.today": "Heute",
|
||||||
|
"watch.tomorrow": "Morgen",
|
||||||
|
"watch.events_today": "Termine",
|
||||||
|
"watch.sign_in": "Auf dem iPhone anmelden",
|
||||||
|
"watch.update": "Watch-App aktualisieren",
|
||||||
|
"watch.waiting": "Warte auf Daten vom iPhone",
|
||||||
|
"watch.unreadable": "Daten nicht lesbar",
|
||||||
|
"watch.as_of": "Stand",
|
||||||
|
"watch.no_information": "Keine Daten nach",
|
||||||
|
],
|
||||||
|
"en": [
|
||||||
|
"watch.title": "Calendarr",
|
||||||
|
"watch.calendars": "Calendars",
|
||||||
|
"watch.allday": "All-day",
|
||||||
|
"watch.no_events": "No events",
|
||||||
|
"watch.now": "now",
|
||||||
|
"watch.today": "Today",
|
||||||
|
"watch.tomorrow": "Tomorrow",
|
||||||
|
"watch.events_today": "events",
|
||||||
|
"watch.sign_in": "Sign in on your iPhone",
|
||||||
|
"watch.update": "Update the watch app",
|
||||||
|
"watch.waiting": "Waiting for data from your iPhone",
|
||||||
|
"watch.unreadable": "Data unreadable",
|
||||||
|
"watch.as_of": "As of",
|
||||||
|
"watch.no_information": "No data after",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Status:** All 17 tasks implemented and build-verified as of 2026-09-11. On-device behaviour is still unverified — see the handoff list in Task 17.
|
||||||
|
|
||||||
**Goal:** Ship a read-only Apple Watch app plus eight watch-face complications that show the next Calendarr appointment, fed from the iPhone over WatchConnectivity.
|
**Goal:** Ship a read-only Apple Watch app plus eight watch-face complications that show the next Calendarr appointment, fed from the iPhone over WatchConnectivity.
|
||||||
|
|
||||||
**Architecture:** The watch is a read-only consumer of the phone's snapshot. Every piece of logic that can be tested lives in `CalendarrCore` (testable with `swift test` on the dev machine); the watch targets hold only thin SwiftUI and WCSession glue, which can only be build-verified here. The phone trims its 42-day snapshot to a 14-day watch payload and sends it; the watch writes it into its *own* App Group container through the same `SnapshotStore`, so there is one wire format and one reader.
|
**Architecture:** The watch is a read-only consumer of the phone's snapshot. Every piece of logic that can be tested lives in `CalendarrCore` (testable with `swift test` on the dev machine); the watch targets hold only thin SwiftUI and WCSession glue, which can only be build-verified here. The phone trims its 42-day snapshot to a 14-day watch payload and sends it; the watch writes it into its *own* App Group container through the same `SnapshotStore`, so there is one wire format and one reader.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Apple Watch app and watch-face complications
|
# Apple Watch app and watch-face complications
|
||||||
|
|
||||||
Date: 2026-09-11
|
Date: 2026-09-11
|
||||||
Status: approved — ready for implementation planning
|
Status: implemented — pending on-device verification
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user