Add Apple Watch app (TickTick-style) + midnight period notifications
WenzaWatch (new watchOS target, embed disabled until the watchOS platform is installed — see project.yml note): - Today list of actionable tasks (dated today/overdue + recurring occurrences), tap-to-complete with haptics, and add-via-dictation. - Reads/writes the same tasks through the shared iCloud KV store that CloudSyncManager mirrors on the phone; watch entitlement uses the iOS app's kvstore identifier so they share one store. New tasks include all required TaskItem keys so the phone decodes them. - NOTE: not compiled here (watchOS SDK absent on this machine); iOS build verified green with the embed commented out. Period notifications: day/week/month/year now all fire at midnight (00:00), adding the daily "One more day passed." (Pretty Progress style). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
754D3DE3CEB998E36E585A61 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0506183945D16EC443A69651 /* LiveActivityManager.swift */; };
|
||||
79FC1DF6762C6F02D01AB643 /* KisaniCalWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; };
|
||||
8387093D19FB3397CCB8FEF8 /* WenzaWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF528FCC224EF283F95851AD /* WenzaWatchApp.swift */; };
|
||||
83EA218392952885C97144D1 /* TodayMenuFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED5DB5340BEB3EF7A78CA153 /* TodayMenuFeatures.swift */; };
|
||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
|
||||
8C9567A3DE3F63A1ECAE84D5 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3D5289C5F93484E22DEB63 /* TutorialView.swift */; };
|
||||
@@ -81,13 +82,14 @@
|
||||
0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
|
||||
0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = "<group>"; };
|
||||
0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
||||
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
||||
208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = "<group>"; };
|
||||
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
|
||||
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
|
||||
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = "<group>"; };
|
||||
35FF274EFF2361B7F3D2D227 /* WenzaWatch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WenzaWatch.entitlements; sourceTree = "<group>"; };
|
||||
42650F655DB8B320C7C03929 /* KisaniCalWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalWidgets.swift; sourceTree = "<group>"; };
|
||||
429806CE1021C8DE2EB770CE /* WidgetViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetViews.swift; sourceTree = "<group>"; };
|
||||
449C34805DC6B2CB66886544 /* CloudSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSyncManager.swift; sourceTree = "<group>"; };
|
||||
@@ -99,15 +101,17 @@
|
||||
72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
72FDF9C8DD37134576356B89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
|
||||
7A9D47B284FD6A217AEF813B /* WenzaWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WenzaWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
89550F2CD19B950CCC6AD37F /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
||||
8DC8687EA2FBA9FB2EEE51C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
8E7FC6446D5C5B2A2D9387DA /* KisaniCalWidgets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCalWidgets.entitlements; sourceTree = "<group>"; };
|
||||
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
|
||||
92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; };
|
||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||
AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
|
||||
B4CFFDFE4653A9E901CEF28D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; };
|
||||
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||
@@ -119,6 +123,7 @@
|
||||
FA3D5289C5F93484E22DEB63 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
|
||||
FC17C7BA15BD2E84AE5F77CF /* EventCountdownWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCountdownWidget.swift; sourceTree = "<group>"; };
|
||||
FEEB07EB89E89383C32ADB34 /* TaskActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskActivityAttributes.swift; sourceTree = "<group>"; };
|
||||
FF528FCC224EF283F95851AD /* WenzaWatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WenzaWatchApp.swift; sourceTree = "<group>"; };
|
||||
FF5AFD143B693B77D07FBDA4 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -141,11 +146,22 @@
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
48146B56E740528496663D47 /* WenzaWatch */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4CFFDFE4653A9E901CEF28D /* Info.plist */,
|
||||
35FF274EFF2361B7F3D2D227 /* WenzaWatch.entitlements */,
|
||||
FF528FCC224EF283F95851AD /* WenzaWatchApp.swift */,
|
||||
);
|
||||
path = WenzaWatch;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4D2F2B3472A5A6D99F2FFCD2 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F70DA4746C68CD405435DAB6 /* KisaniCal */,
|
||||
E8D727EEA0C3A4B8006FB087 /* KisaniCalWidgets */,
|
||||
48146B56E740528496663D47 /* WenzaWatch */,
|
||||
51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -155,6 +171,7 @@
|
||||
children = (
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
|
||||
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */,
|
||||
7A9D47B284FD6A217AEF813B /* WenzaWatch.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -285,6 +302,23 @@
|
||||
productReference = 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
403D989BDC6268361CFFB479 /* WenzaWatch */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6F608724A2EAA63171766586 /* Build configuration list for PBXNativeTarget "WenzaWatch" */;
|
||||
buildPhases = (
|
||||
6DEFD550940764CA28D993CE /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = WenzaWatch;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = WenzaWatch;
|
||||
productReference = 7A9D47B284FD6A217AEF813B /* WenzaWatch.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -292,7 +326,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 2620;
|
||||
LastUpgradeCheck = 1500;
|
||||
TargetAttributes = {
|
||||
28B43516AD88946E21D9BFE0 = {
|
||||
DevelopmentTeam = K8BLMMR883;
|
||||
@@ -302,6 +336,10 @@
|
||||
DevelopmentTeam = K8BLMMR883;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
403D989BDC6268361CFFB479 = {
|
||||
DevelopmentTeam = K8BLMMR883;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */;
|
||||
@@ -320,6 +358,7 @@
|
||||
targets = (
|
||||
297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
|
||||
28B43516AD88946E21D9BFE0 /* KisaniCalWidgets */,
|
||||
403D989BDC6268361CFFB479 /* WenzaWatch */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -339,6 +378,14 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
6DEFD550940764CA28D993CE /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8387093D19FB3397CCB8FEF8 /* WenzaWatchApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
82EA49FE155821D424C46912 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -409,7 +456,6 @@
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Wenza;
|
||||
@@ -421,14 +467,13 @@
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -475,7 +520,6 @@
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -489,8 +533,6 @@
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -503,16 +545,10 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -559,7 +595,6 @@
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -580,30 +615,40 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D3A1329FCB6BBA1E8CC25FFC /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = WenzaWatch/WenzaWatch.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = WenzaWatch/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Wenza;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.watchkitapp;
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.9;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
DF56CDD73E9B177D57AE17FB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -611,6 +656,24 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F00C7E0C784B90820717D614 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = WenzaWatch/WenzaWatch.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = WenzaWatch/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Wenza;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.watchkitapp;
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.9;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
F364512BEB70ECB7CB83FBFE /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -619,7 +682,6 @@
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Wenza;
|
||||
@@ -631,14 +693,13 @@
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -658,6 +719,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
6F608724A2EAA63171766586 /* Build configuration list for PBXNativeTarget "WenzaWatch" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
F00C7E0C784B90820717D614 /* Debug */,
|
||||
D3A1329FCB6BBA1E8CC25FFC /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -26,7 +27,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -38,6 +40,8 @@
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -59,6 +63,8 @@
|
||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -76,6 +82,8 @@
|
||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -136,7 +136,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
||||
|
||||
// MARK: - Period milestones ("one more week / month / year passed")
|
||||
|
||||
private static let periodIds = ["kisani.period.week", "kisani.period.month", "kisani.period.year"]
|
||||
private static let periodIds = ["kisani.period.day", "kisani.period.week", "kisani.period.month", "kisani.period.year"]
|
||||
|
||||
/// Recurring nudges at the close of each week, month, and year. Fixed identifiers
|
||||
/// + remove-then-add keep them de-duplicated across reschedules. Disabled (and
|
||||
@@ -155,17 +155,21 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
||||
trigger: UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)))
|
||||
}
|
||||
|
||||
// End of week → Sunday 20:00
|
||||
var week = DateComponents(); week.weekday = 1; week.hour = 20; week.minute = 0
|
||||
add(Self.periodIds[0], "This Week", "One more week passed.", week)
|
||||
// Every midnight → a new day began
|
||||
var day = DateComponents(); day.hour = 0; day.minute = 0
|
||||
add(Self.periodIds[0], "Today", "One more day passed.", day)
|
||||
|
||||
// End of month → 1st of the next month, 09:00
|
||||
var month = DateComponents(); month.day = 1; month.hour = 9; month.minute = 0
|
||||
add(Self.periodIds[1], "This Month", "One more month passed.", month)
|
||||
// Start of the week (Mon 00:00) → the week that just ended
|
||||
var week = DateComponents(); week.weekday = 2; week.hour = 0; week.minute = 0
|
||||
add(Self.periodIds[1], "This Week", "One more week passed.", week)
|
||||
|
||||
// End of year → Jan 1, 09:00
|
||||
var year = DateComponents(); year.month = 1; year.day = 1; year.hour = 9; year.minute = 0
|
||||
add(Self.periodIds[2], "This Year", "One more year passed.", year)
|
||||
// 1st of the month, 00:00 → the month that just ended
|
||||
var month = DateComponents(); month.day = 1; month.hour = 0; month.minute = 0
|
||||
add(Self.periodIds[2], "This Month", "One more month passed.", month)
|
||||
|
||||
// Jan 1, 00:00 → the year that just ended
|
||||
var year = DateComponents(); year.month = 1; year.day = 1; year.hour = 0; year.minute = 0
|
||||
add(Self.periodIds[3], "This Year", "One more year passed.", year)
|
||||
}
|
||||
|
||||
// MARK: - Recurring task completion confirmation
|
||||
|
||||
28
WenzaWatch/Info.plist
Normal file
28
WenzaWatch/Info.plist
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Wenza</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>WKApplication</key>
|
||||
<true/>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>com.kutesir.KisaniCal</string>
|
||||
</dict>
|
||||
</plist>
|
||||
8
WenzaWatch/WenzaWatch.entitlements
Normal file
8
WenzaWatch/WenzaWatch.entitlements
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)com.kutesir.KisaniCal</string>
|
||||
</dict>
|
||||
</plist>
|
||||
221
WenzaWatch/WenzaWatchApp.swift
Normal file
221
WenzaWatch/WenzaWatchApp.swift
Normal file
@@ -0,0 +1,221 @@
|
||||
import SwiftUI
|
||||
import WatchKit
|
||||
|
||||
// MARK: - Shared iCloud task store
|
||||
//
|
||||
// The Watch is a separate device and can't read the iPhone's App Group, so it
|
||||
// reads/writes the SAME tasks via the iCloud key-value store (NSUbiquitousKeyValueStore)
|
||||
// that CloudSyncManager already mirrors on the phone. Writes here propagate back to
|
||||
// the phone, which applies external KV changes into its App Group.
|
||||
|
||||
private func tasksKey() -> String {
|
||||
let uid = NSUbiquitousKeyValueStore.default.string(forKey: "kisani.activeUserId") ?? "shared"
|
||||
return "kisani.tasks.v2.\(uid)"
|
||||
}
|
||||
|
||||
private let occFmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f
|
||||
}()
|
||||
|
||||
struct WatchTask: Identifiable {
|
||||
let id: UUID
|
||||
var title: String
|
||||
var dueDate: Date?
|
||||
var isComplete: Bool
|
||||
var isRecurring: Bool
|
||||
var recurrenceLabel: String?
|
||||
var completedOccurrences: [String]
|
||||
}
|
||||
|
||||
final class WatchStore: ObservableObject {
|
||||
@Published var tasks: [WatchTask] = []
|
||||
private let kv = NSUbiquitousKeyValueStore.default
|
||||
private let cal = Calendar.current
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(externalChange),
|
||||
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: kv)
|
||||
kv.synchronize()
|
||||
load()
|
||||
}
|
||||
|
||||
@objc private func externalChange() {
|
||||
kv.synchronize()
|
||||
DispatchQueue.main.async { [weak self] in self?.load() }
|
||||
}
|
||||
|
||||
func load() {
|
||||
guard let data = kv.data(forKey: tasksKey()),
|
||||
let arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]] else {
|
||||
tasks = []; return
|
||||
}
|
||||
tasks = arr.compactMap { d in
|
||||
guard let idStr = d["id"] as? String, let id = UUID(uuidString: idStr),
|
||||
let title = d["title"] as? String else { return nil }
|
||||
let due = (d["dueDate"] as? Double).map { Date(timeIntervalSinceReferenceDate: $0) }
|
||||
return WatchTask(id: id, title: title, dueDate: due,
|
||||
isComplete: d["isComplete"] as? Bool ?? false,
|
||||
isRecurring: d["isRecurring"] as? Bool ?? false,
|
||||
recurrenceLabel: d["recurrenceLabel"] as? String,
|
||||
completedOccurrences: d["completedOccurrences"] as? [String] ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
// Today's actionable tasks: dated today/overdue, plus recurring occurrences due today.
|
||||
var todayTasks: [WatchTask] {
|
||||
let end = cal.date(byAdding: .day, value: 1, to: cal.startOfDay(for: Date()))!
|
||||
return tasks.filter { t in
|
||||
if t.isRecurring { return occursToday(t) && !completedToday(t) }
|
||||
guard !t.isComplete, let due = t.dueDate else { return false }
|
||||
return due < end
|
||||
}
|
||||
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
||||
}
|
||||
|
||||
private func completedToday(_ t: WatchTask) -> Bool {
|
||||
t.completedOccurrences.contains(occFmt.string(from: Date()))
|
||||
}
|
||||
|
||||
private func occursToday(_ t: WatchTask) -> Bool {
|
||||
guard let due = t.dueDate else { return false }
|
||||
let now = Date()
|
||||
if due > cal.date(byAdding: .day, value: 1, to: cal.startOfDay(for: now))! { return false }
|
||||
switch t.recurrenceLabel {
|
||||
case "Daily": return true
|
||||
case "Every Weekday": let wd = cal.component(.weekday, from: now); return wd >= 2 && wd <= 6
|
||||
case "Weekly": return cal.component(.weekday, from: due) == cal.component(.weekday, from: now)
|
||||
case "Monthly": return cal.component(.day, from: due) == cal.component(.day, from: now)
|
||||
case "Yearly": return cal.component(.month, from: due) == cal.component(.month, from: now)
|
||||
&& cal.component(.day, from: due) == cal.component(.day, from: now)
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
func toggle(_ task: WatchTask) {
|
||||
mutate { arr in
|
||||
for i in arr.indices where (arr[i]["id"] as? String) == task.id.uuidString {
|
||||
if task.isRecurring {
|
||||
var occ = arr[i]["completedOccurrences"] as? [String] ?? []
|
||||
let k = occFmt.string(from: Date())
|
||||
if let idx = occ.firstIndex(of: k) { occ.remove(at: idx) } else { occ.append(k) }
|
||||
arr[i]["completedOccurrences"] = occ
|
||||
} else {
|
||||
let done = (arr[i]["isComplete"] as? Bool) ?? false
|
||||
arr[i]["isComplete"] = !done
|
||||
if !done { arr[i]["completedAt"] = Date().timeIntervalSinceReferenceDate }
|
||||
else { arr[i].removeValue(forKey: "completedAt") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func add(_ title: String) {
|
||||
let t = title.trimmingCharacters(in: .whitespaces)
|
||||
guard !t.isEmpty else { return }
|
||||
mutate { arr in
|
||||
// All non-optional TaskItem keys must be present (synthesized Decodable
|
||||
// throws on missing keys); raw values mirror the iOS model.
|
||||
arr.append([
|
||||
"id": UUID().uuidString,
|
||||
"title": t,
|
||||
"dueDate": self.cal.startOfDay(for: Date()).timeIntervalSinceReferenceDate,
|
||||
"hasTime": false,
|
||||
"isComplete": false,
|
||||
"quadrant": "Urgent + Important",
|
||||
"category": "Reminder",
|
||||
"taskColor": "orange",
|
||||
"isRecurring": false,
|
||||
"isPinned": false,
|
||||
"isAllDay": false,
|
||||
"priority": "None",
|
||||
"constantReminder": false,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private func mutate(_ change: (inout [[String: Any]]) -> Void) {
|
||||
let key = tasksKey()
|
||||
var arr: [[String: Any]] = []
|
||||
if let data = kv.data(forKey: key),
|
||||
let parsed = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]] { arr = parsed }
|
||||
change(&arr)
|
||||
if let out = try? JSONSerialization.data(withJSONObject: arr) {
|
||||
kv.set(out, forKey: key)
|
||||
kv.synchronize()
|
||||
}
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
@main
|
||||
struct WenzaWatchApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup { TaskListView() }
|
||||
}
|
||||
}
|
||||
|
||||
struct TaskListView: View {
|
||||
@StateObject private var store = WatchStore()
|
||||
@State private var showAdd = false
|
||||
|
||||
private let timeFmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "h:mm a"; return f
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Button { showAdd = true } label: {
|
||||
Label("Add Task", systemImage: "plus.circle.fill")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
|
||||
if store.todayTasks.isEmpty {
|
||||
Text("All caught up")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.footnote)
|
||||
} else {
|
||||
ForEach(store.todayTasks) { task in
|
||||
Button { withAnimation { WKInterfaceDevice.current().play(.success); store.toggle(task) } } label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "circle")
|
||||
.foregroundStyle(.orange)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(task.title).lineLimit(2)
|
||||
if let due = task.dueDate, !task.isRecurring {
|
||||
Text(timeFmt.string(from: due))
|
||||
.font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Today")
|
||||
.sheet(isPresented: $showAdd) {
|
||||
AddTaskView { store.add($0); showAdd = false }
|
||||
}
|
||||
.onAppear { store.reload() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddTaskView: View {
|
||||
let onAdd: (String) -> Void
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var text = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
TextField("New task", text: $text) // Watch dictation / Scribble
|
||||
Button("Add") { onAdd(text) }
|
||||
.disabled(text.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
Button("Cancel", role: .cancel) { dismiss() }
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
29
project.yml
29
project.yml
@@ -38,6 +38,11 @@ targets:
|
||||
dependencies:
|
||||
- target: KisaniCalWidgets
|
||||
embed: true
|
||||
# WenzaWatch is built/embedded only when the watchOS platform is installed.
|
||||
# Re-enable shipping it on the Watch by uncommenting the embed below (requires
|
||||
# Xcode > Settings > Components > watchOS):
|
||||
# - target: WenzaWatch
|
||||
# embed: true
|
||||
settings:
|
||||
base:
|
||||
SWIFT_VERSION: 5.9
|
||||
@@ -85,3 +90,27 @@ targets:
|
||||
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
|
||||
APPLICATION_EXTENSION_API_ONLY: YES
|
||||
CODE_SIGN_ENTITLEMENTS: KisaniCalWidgets/KisaniCalWidgets.entitlements
|
||||
|
||||
WenzaWatch:
|
||||
type: application
|
||||
platform: watchOS
|
||||
deploymentTarget: "10.0"
|
||||
sources:
|
||||
- path: WenzaWatch
|
||||
info:
|
||||
path: WenzaWatch/Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: "Wenza"
|
||||
CFBundleShortVersionString: "$(MARKETING_VERSION)"
|
||||
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
|
||||
WKApplication: true
|
||||
WKCompanionAppBundleIdentifier: com.kutesir.KisaniCal
|
||||
settings:
|
||||
base:
|
||||
SWIFT_VERSION: 5.9
|
||||
GENERATE_INFOPLIST_FILE: NO
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.kutesir.KisaniCal.watchkitapp
|
||||
TARGETED_DEVICE_FAMILY: "4"
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
CODE_SIGN_ENTITLEMENTS: WenzaWatch/WenzaWatch.entitlements
|
||||
INFOPLIST_KEY_CFBundleDisplayName: "Wenza"
|
||||
|
||||
Reference in New Issue
Block a user