feat: add widget extension — small, medium, large
- KisaniCalWidget: 3 sizes sharing the app's design language Small: date badge + kisaniCAL. wordmark + task count Medium: badge + task list (3 items) + workout row Large: badge + full task list (6 items) + workout row - WidgetDataStore (Shared/): lightweight Codable types for App Group UserDefaults cross-process data exchange - WidgetBridge: TaskViewModel/WorkoutViewModel extensions that sync data into App Group and call WidgetCenter.reloadAllTimelines() - ContentView: calls syncWidget() on appear and scene becoming active - project.yml + entitlements updated; project regenerated via xcodegen
This commit is contained in:
@@ -10,7 +10,12 @@
|
||||
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
|
||||
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; };
|
||||
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; };
|
||||
3256F64040E5BBCD30644047 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */; };
|
||||
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; };
|
||||
46D78859DD34FDE6172F213D /* KisaniCalWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
4F829CF0B3BD04CD2EAB8C73 /* WidgetBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */; };
|
||||
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106EEF572C6F8990408329F0 /* OnboardingView.swift */; };
|
||||
6921CB73A3257502FF778381 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */; };
|
||||
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; };
|
||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
|
||||
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF6CCD95A587E26E30F5712 /* CalendarView.swift */; };
|
||||
@@ -19,35 +24,74 @@
|
||||
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
|
||||
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */; };
|
||||
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
|
||||
C1B3FC29820FCAF069D2C6FE /* KisaniCalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */; };
|
||||
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
|
||||
CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */; };
|
||||
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
|
||||
E7F1A2B3C4D5E6F7A8B9C0D1 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */; };
|
||||
B1C2D3E4F5A6B7C8D9E0F1A2 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */; };
|
||||
E2F3A4B5C6D7E8F9A0B1C2D3 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E4F5A6B7C8D9E0F1A2B3C4 /* SplashView.swift */; };
|
||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
D4A1B57264F223C8871E1BC9 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = AF6DE7A812408E3742522E90 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 532C52E96C1EE1E30A7E9157;
|
||||
remoteInfo = KisaniCalWidget;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
46D78859DD34FDE6172F213D /* KisaniCalWidget.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalApp.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = "<group>"; };
|
||||
57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
74EE7C2E74094E3E27FD78E4 /* KisaniCalWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCalWidget.entitlements; sourceTree = "<group>"; };
|
||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalWidget.swift; sourceTree = "<group>"; };
|
||||
8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBridge.swift; sourceTree = "<group>"; };
|
||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = "<group>"; };
|
||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; 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>"; };
|
||||
C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
|
||||
C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||
D3E4F5A6B7C8D9E0F1A2B3C4 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
|
||||
D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; };
|
||||
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
|
||||
F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
10E8E769F7D262FF425B460B /* KisaniCalWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
74EE7C2E74094E3E27FD78E4 /* KisaniCalWidget.entitlements */,
|
||||
825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */,
|
||||
);
|
||||
path = KisaniCalWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
21B93C269F283F11B415B18C /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -56,6 +100,14 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2217B7EEC45957B820311EC7 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
23CBCF100C5EF55E737379CA /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -69,6 +121,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F70DA4746C68CD405435DAB6 /* KisaniCal */,
|
||||
10E8E769F7D262FF425B460B /* KisaniCalWidget */,
|
||||
2217B7EEC45957B820311EC7 /* Shared */,
|
||||
51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -77,6 +131,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
|
||||
363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -87,11 +142,11 @@
|
||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
|
||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
|
||||
D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
|
||||
106EEF572C6F8990408329F0 /* OnboardingView.swift */,
|
||||
72308FEE0226F45414C04DDD /* SettingsView.swift */,
|
||||
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */,
|
||||
C786EBC7DF879D64EB28165E /* TodayView.swift */,
|
||||
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */,
|
||||
C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */,
|
||||
D3E4F5A6B7C8D9E0F1A2B3C4 /* SplashView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -112,22 +167,16 @@
|
||||
path = Theme;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A1B2C3D4E5F6070A8B9C0D1E /* Managers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */,
|
||||
);
|
||||
path = Managers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F70DA4746C68CD405435DAB6 /* KisaniCal */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
72FDF9C8DD37134576356B89 /* Assets.xcassets */,
|
||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
|
||||
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */,
|
||||
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
|
||||
8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */,
|
||||
21B93C269F283F11B415B18C /* Components */,
|
||||
A1B2C3D4E5F6070A8B9C0D1E /* Managers */,
|
||||
FB9BF734B9E493EEB09ACE21 /* Managers */,
|
||||
23CBCF100C5EF55E737379CA /* Models */,
|
||||
9981C53A4E263A0DBC3DF194 /* Preview Content */,
|
||||
E0A03E79A679740978E61BF1 /* Theme */,
|
||||
@@ -136,6 +185,14 @@
|
||||
path = KisaniCal;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB9BF734B9E493EEB09ACE21 /* Managers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
|
||||
);
|
||||
path = Managers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -145,10 +202,12 @@
|
||||
buildPhases = (
|
||||
82EA49FE155821D424C46912 /* Sources */,
|
||||
E1F1A3EE4469B851B972E4AE /* Resources */,
|
||||
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3FA0FA885B1A4FC539D57049 /* PBXTargetDependency */,
|
||||
);
|
||||
name = KisaniCal;
|
||||
packageProductDependencies = (
|
||||
@@ -157,6 +216,23 @@
|
||||
productReference = 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6B8469B838ACDD00336D1CAD /* Build configuration list for PBXNativeTarget "KisaniCalWidget" */;
|
||||
buildPhases = (
|
||||
B3D66A55B74C7B9AE6099149 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = KisaniCalWidget;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = KisaniCalWidget;
|
||||
productReference = 363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -169,6 +245,9 @@
|
||||
297C0C2BFBA10AB52D5D49CE = {
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
532C52E96C1EE1E30A7E9157 = {
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */;
|
||||
@@ -186,6 +265,7 @@
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
|
||||
532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -214,28 +294,47 @@
|
||||
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */,
|
||||
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
|
||||
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
|
||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */,
|
||||
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */,
|
||||
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */,
|
||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */,
|
||||
6921CB73A3257502FF778381 /* SplashView.swift in Sources */,
|
||||
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */,
|
||||
E7F1A2B3C4D5E6F7A8B9C0D1 /* NotificationManager.swift in Sources */,
|
||||
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */,
|
||||
4F829CF0B3BD04CD2EAB8C73 /* WidgetBridge.swift in Sources */,
|
||||
3256F64040E5BBCD30644047 /* WidgetDataStore.swift in Sources */,
|
||||
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */,
|
||||
B1C2D3E4F5A6B7C8D9E0F1A2 /* OnboardingView.swift in Sources */,
|
||||
E2F3A4B5C6D7E8F9A0B1C2D3 /* SplashView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B3D66A55B74C7B9AE6099149 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C1B3FC29820FCAF069D2C6FE /* KisaniCalWidget.swift in Sources */,
|
||||
CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
3FA0FA885B1A4FC539D57049 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */;
|
||||
targetProxy = D4A1B57264F223C8871E1BC9 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0CEA6637F45C32A237FBA20D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||
@@ -314,6 +413,40 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
47C8DA15444717168992CD67 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidget/KisaniCalWidget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSExtensionPointIdentifier = "com.apple.widgetkit-extension";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.widget;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.9;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
9F668AA275094D23AACA2F61 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidget/KisaniCalWidget.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSExtensionPointIdentifier = "com.apple.widgetkit-extension";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.widget;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.9;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BD527AA54887489CC665FB9D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -382,9 +515,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||
@@ -410,6 +543,15 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
6B8469B838ACDD00336D1CAD /* Build configuration list for PBXNativeTarget "KisaniCalWidget" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
47C8DA15444717168992CD67 /* Debug */,
|
||||
9F668AA275094D23AACA2F61 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Debug;
|
||||
};
|
||||
B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
7
KisaniCal.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
KisaniCal.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -73,12 +73,19 @@ struct ContentView: View {
|
||||
OnboardingView(isPresented: $showOnboarding)
|
||||
.onDisappear { hasOnboarded = true }
|
||||
}
|
||||
.onAppear { if !hasOnboarded { showOnboarding = true } }
|
||||
.onAppear {
|
||||
if !hasOnboarded { showOnboarding = true }
|
||||
taskVM.syncWidget()
|
||||
workoutVM.syncWidget()
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .active {
|
||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||
taskVM.syncWidget()
|
||||
workoutVM.syncWidget()
|
||||
}
|
||||
}
|
||||
.onChange(of: taskVM.tasks.count) { _ in taskVM.syncWidget() }
|
||||
.onChange(of: workoutVM.doneSets) { _ in
|
||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||
}
|
||||
|
||||
10
KisaniCal/KisaniCal.entitlements
Normal file
10
KisaniCal/KisaniCal.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.kutesir.KisaniCal</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
41
KisaniCal/WidgetBridge.swift
Normal file
41
KisaniCal/WidgetBridge.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import WidgetKit
|
||||
|
||||
// Syncs app ViewModels into App Group UserDefaults so the widget can read them,
|
||||
// then asks WidgetKit to reload all timelines.
|
||||
|
||||
extension TaskViewModel {
|
||||
func syncWidget() {
|
||||
let cal = Calendar.current
|
||||
let start = cal.startOfDay(for: Date())
|
||||
let end = cal.date(byAdding: .day, value: 1, to: start)!
|
||||
|
||||
let entries = tasks
|
||||
.filter { !$0.isComplete }
|
||||
.prefix(10)
|
||||
.map { t in
|
||||
WidgetDataStore.WidgetTask(
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
dueDate: t.dueDate,
|
||||
isToday: t.dueDate.map { $0 >= start && $0 < end } ?? false,
|
||||
isOverdue: t.dueDate.map { $0 < start } ?? false
|
||||
)
|
||||
}
|
||||
WidgetDataStore.saveTasks(Array(entries))
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutViewModel {
|
||||
func syncWidget() {
|
||||
let weekday = Calendar.current.component(.weekday, from: Date())
|
||||
let todayProgram = program(for: Date())
|
||||
let workout = WidgetDataStore.WidgetWorkout(
|
||||
name: activeProgram?.name ?? "Workout",
|
||||
sectionsCount: activeProgram?.sections.count ?? 0,
|
||||
hasWorkoutToday: todayProgram != nil
|
||||
)
|
||||
WidgetDataStore.saveWorkout(workout)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
10
KisaniCalWidget/KisaniCalWidget.entitlements
Normal file
10
KisaniCalWidget/KisaniCalWidget.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.kutesir.KisaniCal</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
414
KisaniCalWidget/KisaniCalWidget.swift
Normal file
414
KisaniCalWidget/KisaniCalWidget.swift
Normal file
@@ -0,0 +1,414 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Colors (no UIKit dependency)
|
||||
|
||||
private enum WC {
|
||||
static let accent = Color(red: 232/255, green: 98/255, blue: 42/255)
|
||||
static let green = Color(red: 62/255, green: 207/255, blue: 142/255)
|
||||
static let blue = Color(red: 77/255, green: 157/255, blue: 224/255)
|
||||
|
||||
static func bg(_ s: ColorScheme) -> Color {
|
||||
s == .dark ? Color(red: 9/255, green: 9/255, blue: 9/255)
|
||||
: Color(red: 245/255, green: 243/255, blue: 240/255)
|
||||
}
|
||||
static func surface(_ s: ColorScheme) -> Color {
|
||||
s == .dark ? Color(red: 19/255, green: 19/255, blue: 19/255)
|
||||
: Color(red: 255/255, green: 253/255, blue: 250/255)
|
||||
}
|
||||
static func text(_ s: ColorScheme) -> Color {
|
||||
s == .dark ? Color(red: 240/255, green: 237/255, blue: 232/255)
|
||||
: Color(red: 26/255, green: 23/255, blue: 20/255)
|
||||
}
|
||||
static func text2(_ s: ColorScheme) -> Color { text(s).opacity(0.48) }
|
||||
static func text3(_ s: ColorScheme) -> Color { text(s).opacity(0.30) }
|
||||
static func border(_ s: ColorScheme) -> Color {
|
||||
s == .dark ? Color.white.opacity(0.07) : Color.black.opacity(0.09)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Timeline Entry
|
||||
|
||||
struct KisaniEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let todayTasks: [WidgetDataStore.WidgetTask]
|
||||
let overdueTasks: [WidgetDataStore.WidgetTask]
|
||||
let workout: WidgetDataStore.WidgetWorkout?
|
||||
}
|
||||
|
||||
// MARK: - Provider
|
||||
|
||||
struct KisaniProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> KisaniEntry {
|
||||
KisaniEntry(
|
||||
date: Date(),
|
||||
todayTasks: previewTasks,
|
||||
overdueTasks: [],
|
||||
workout: WidgetDataStore.WidgetWorkout(
|
||||
name: "Shoulders & Arms", sectionsCount: 4, hasWorkoutToday: true)
|
||||
)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (KisaniEntry) -> Void) {
|
||||
completion(entry())
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<KisaniEntry>) -> Void) {
|
||||
let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
|
||||
completion(Timeline(entries: [entry()], policy: .after(next)))
|
||||
}
|
||||
|
||||
private func entry() -> KisaniEntry {
|
||||
let all = WidgetDataStore.loadTasks()
|
||||
let today = all.filter { $0.isToday }
|
||||
let overdue = all.filter { $0.isOverdue }
|
||||
let workout = WidgetDataStore.loadWorkout()
|
||||
return KisaniEntry(date: Date(), todayTasks: today, overdueTasks: overdue, workout: workout)
|
||||
}
|
||||
|
||||
private var previewTasks: [WidgetDataStore.WidgetTask] {
|
||||
[
|
||||
.init(id: UUID(), title: "Azure exam prep", dueDate: Date(), isToday: true, isOverdue: false),
|
||||
.init(id: UUID(), title: "Push AWS project", dueDate: Date(), isToday: true, isOverdue: false),
|
||||
.init(id: UUID(), title: "Review PR #42", dueDate: Date(), isToday: false, isOverdue: true),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widget Bundle
|
||||
|
||||
@main
|
||||
struct KisaniCalWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
KisaniCalWidget()
|
||||
}
|
||||
}
|
||||
|
||||
struct KisaniCalWidget: Widget {
|
||||
let kind = "KisaniCalWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
|
||||
KisaniWidgetView(entry: entry)
|
||||
.containerBackground(for: .widget) { Color.clear }
|
||||
}
|
||||
.configurationDisplayName("Kisani Cal")
|
||||
.description("Today's tasks and workout at a glance.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Root Widget View
|
||||
|
||||
struct KisaniWidgetView: View {
|
||||
@Environment(\.widgetFamily) private var family
|
||||
let entry: KisaniEntry
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemSmall: SmallWidget(entry: entry)
|
||||
case .systemMedium: MediumWidget(entry: entry)
|
||||
default: LargeWidget(entry: entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shared Sub-Views
|
||||
|
||||
private struct DateBadge: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let size: CGFloat
|
||||
|
||||
private static let monthFmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "MMM"; return f
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
let day = Calendar.current.component(.day, from: Date())
|
||||
VStack(spacing: 0) {
|
||||
Text(Self.monthFmt.string(from: Date()).uppercased())
|
||||
.font(.system(size: size * 0.18, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
.kerning(1)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: size * 0.32)
|
||||
.background(WC.accent)
|
||||
Text("\(day)")
|
||||
.font(.system(size: size * 0.48, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(WC.text(cs))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: size * 0.68)
|
||||
.background(WC.surface(cs))
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(RoundedRectangle(cornerRadius: size * 0.22))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: size * 0.22)
|
||||
.stroke(WC.border(cs), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TaskRow: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let task: WidgetDataStore.WidgetTask
|
||||
|
||||
private static let fmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "d MMM"; return f
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 5) {
|
||||
Circle()
|
||||
.stroke(task.isOverdue ? WC.accent : WC.text3(cs), lineWidth: 1.2)
|
||||
.frame(width: 7, height: 7)
|
||||
Text(task.title)
|
||||
.font(.system(size: 11, weight: .regular, design: .default))
|
||||
.foregroundColor(WC.text(cs))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
if let d = task.dueDate {
|
||||
Text(task.isOverdue ? "Overdue" : Self.fmt.string(from: d))
|
||||
.font(.system(size: 9, weight: .regular, design: .monospaced))
|
||||
.foregroundColor(task.isOverdue ? WC.accent : WC.text3(cs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkoutRow: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let workout: WidgetDataStore.WidgetWorkout
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 6) {
|
||||
WorkoutSegBar(count: workout.sectionsCount)
|
||||
.frame(width: 22, height: 22)
|
||||
Text(workout.name)
|
||||
.font(.system(size: 11, weight: .semibold, design: .default))
|
||||
.foregroundColor(WC.text(cs))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
Text("Workout")
|
||||
.font(.system(size: 9, weight: .regular, design: .monospaced))
|
||||
.foregroundColor(WC.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct WorkoutSegBar: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let count: Int
|
||||
|
||||
var body: some View {
|
||||
let n = max(1, min(6, count))
|
||||
VStack(spacing: 1.5) {
|
||||
ForEach(0..<n, id: \.self) { _ in
|
||||
RoundedRectangle(cornerRadius: 1)
|
||||
.fill(WC.blue)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.padding(4)
|
||||
.background(WC.blue.opacity(0.12))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Small Widget
|
||||
|
||||
private struct SmallWidget: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let entry: KisaniEntry
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
WC.bg(cs).ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
DateBadge(size: 72)
|
||||
|
||||
VStack(spacing: 2) {
|
||||
HStack(spacing: 0) {
|
||||
Text("kisani")
|
||||
.font(.system(size: 11, weight: .regular, design: .monospaced))
|
||||
.foregroundColor(WC.text2(cs))
|
||||
Text("CAL")
|
||||
.font(.system(size: 11, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(WC.text(cs))
|
||||
Text(".")
|
||||
.font(.system(size: 11, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(WC.accent)
|
||||
}
|
||||
|
||||
let todayCount = entry.todayTasks.count
|
||||
let overdueCount = entry.overdueTasks.count
|
||||
Group {
|
||||
if overdueCount > 0 {
|
||||
Text("\(overdueCount) overdue · \(todayCount) today")
|
||||
.foregroundColor(WC.accent)
|
||||
} else if todayCount > 0 {
|
||||
Text("\(todayCount) task\(todayCount == 1 ? "" : "s") today")
|
||||
.foregroundColor(WC.text2(cs))
|
||||
} else {
|
||||
Text("All clear")
|
||||
.foregroundColor(WC.text3(cs))
|
||||
}
|
||||
}
|
||||
.font(.system(size: 9, weight: .regular, design: .monospaced))
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Medium Widget
|
||||
|
||||
private struct MediumWidget: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let entry: KisaniEntry
|
||||
|
||||
private var displayTasks: [WidgetDataStore.WidgetTask] {
|
||||
let all = entry.overdueTasks + entry.todayTasks
|
||||
return Array(all.prefix(3))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
WC.bg(cs).ignoresSafeArea()
|
||||
|
||||
HStack(alignment: .top, spacing: 14) {
|
||||
// Left: badge + brand
|
||||
VStack(spacing: 8) {
|
||||
DateBadge(size: 64)
|
||||
HStack(spacing: 0) {
|
||||
Text("kisani")
|
||||
.font(.system(size: 9, weight: .regular, design: .monospaced))
|
||||
.foregroundColor(WC.text2(cs))
|
||||
Text("CAL.")
|
||||
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(WC.accent)
|
||||
}
|
||||
}
|
||||
|
||||
// Right: tasks + workout
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
if displayTasks.isEmpty && entry.workout == nil {
|
||||
Spacer()
|
||||
Text("Nothing scheduled")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundColor(WC.text3(cs))
|
||||
Spacer()
|
||||
} else {
|
||||
ForEach(displayTasks) { task in
|
||||
TaskRow(task: task)
|
||||
}
|
||||
if let workout = entry.workout, workout.hasWorkoutToday,
|
||||
displayTasks.count < 3 {
|
||||
if !displayTasks.isEmpty {
|
||||
Divider().opacity(0.4)
|
||||
}
|
||||
WorkoutRow(workout: workout)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(14)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Large Widget
|
||||
|
||||
private struct LargeWidget: View {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
let entry: KisaniEntry
|
||||
|
||||
private static let dayFmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "EEEE, MMM d"; return f
|
||||
}()
|
||||
|
||||
private var allTasks: [WidgetDataStore.WidgetTask] {
|
||||
Array((entry.overdueTasks + entry.todayTasks).prefix(6))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
WC.bg(cs).ignoresSafeArea()
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header row
|
||||
HStack(spacing: 12) {
|
||||
DateBadge(size: 56)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("kisani")
|
||||
.font(.system(size: 14, weight: .regular, design: .monospaced))
|
||||
.foregroundColor(WC.text2(cs))
|
||||
Text("CAL")
|
||||
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(WC.text(cs))
|
||||
Text(".")
|
||||
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(WC.accent)
|
||||
}
|
||||
Text(Self.dayFmt.string(from: Date()))
|
||||
.font(.system(size: 10, design: .monospaced))
|
||||
.foregroundColor(WC.text2(cs))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
let total = entry.todayTasks.count + entry.overdueTasks.count
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text("\(total)")
|
||||
.font(.system(size: 22, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(entry.overdueTasks.isEmpty ? WC.text(cs) : WC.accent)
|
||||
Text("tasks")
|
||||
.font(.system(size: 9, design: .monospaced))
|
||||
.foregroundColor(WC.text3(cs))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.top, 14)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Divider().opacity(0.4).padding(.horizontal, 14)
|
||||
|
||||
// Task list
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
if allTasks.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Nothing due today")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.foregroundColor(WC.text3(cs))
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
} else {
|
||||
ForEach(allTasks) { task in
|
||||
TaskRow(task: task)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// Workout row
|
||||
if let workout = entry.workout, workout.hasWorkoutToday {
|
||||
Divider().opacity(0.4).padding(.horizontal, 14)
|
||||
WorkoutRow(workout: workout)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Shared/WidgetDataStore.swift
Normal file
61
Shared/WidgetDataStore.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
// Shared between app and widget extension via App Group UserDefaults.
|
||||
// Types are pure Codable — no UIKit / AppColors dependency.
|
||||
|
||||
struct WidgetDataStore {
|
||||
static let suiteName = "group.com.kutesir.KisaniCal"
|
||||
static let tasksKey = "kisani.widget.tasks"
|
||||
static let workoutKey = "kisani.widget.workout"
|
||||
|
||||
// MARK: - Shared types
|
||||
|
||||
struct WidgetTask: Codable, Identifiable {
|
||||
let id: UUID
|
||||
let title: String
|
||||
let dueDate: Date?
|
||||
let isToday: Bool
|
||||
let isOverdue: Bool
|
||||
}
|
||||
|
||||
struct WidgetWorkout: Codable {
|
||||
let name: String
|
||||
let sectionsCount: Int
|
||||
let hasWorkoutToday: Bool
|
||||
}
|
||||
|
||||
// MARK: - Write (app target)
|
||||
|
||||
static func saveTasks(_ tasks: [WidgetTask]) {
|
||||
guard let data = try? JSONEncoder().encode(tasks),
|
||||
let ud = UserDefaults(suiteName: suiteName) else { return }
|
||||
ud.set(data, forKey: tasksKey)
|
||||
}
|
||||
|
||||
static func saveWorkout(_ workout: WidgetWorkout?) {
|
||||
guard let ud = UserDefaults(suiteName: suiteName) else { return }
|
||||
if let workout, let data = try? JSONEncoder().encode(workout) {
|
||||
ud.set(data, forKey: workoutKey)
|
||||
} else {
|
||||
ud.removeObject(forKey: workoutKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Read (widget target)
|
||||
|
||||
static func loadTasks() -> [WidgetTask] {
|
||||
guard let ud = UserDefaults(suiteName: suiteName),
|
||||
let data = ud.data(forKey: tasksKey),
|
||||
let list = try? JSONDecoder().decode([WidgetTask].self, from: data)
|
||||
else { return [] }
|
||||
return list
|
||||
}
|
||||
|
||||
static func loadWorkout() -> WidgetWorkout? {
|
||||
guard let ud = UserDefaults(suiteName: suiteName),
|
||||
let data = ud.data(forKey: workoutKey),
|
||||
let workout = try? JSONDecoder().decode(WidgetWorkout.self, from: data)
|
||||
else { return nil }
|
||||
return workout
|
||||
}
|
||||
}
|
||||
23
project.yml
23
project.yml
@@ -13,6 +13,7 @@ targets:
|
||||
deploymentTarget: "16.0"
|
||||
sources:
|
||||
- path: KisaniCal
|
||||
- path: Shared
|
||||
settings:
|
||||
base:
|
||||
SWIFT_VERSION: 5.9
|
||||
@@ -28,5 +29,27 @@ targets:
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
ENABLE_PREVIEWS: YES
|
||||
CODE_SIGN_ENTITLEMENTS: KisaniCal/KisaniCal.entitlements
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
|
||||
dependencies:
|
||||
- target: KisaniCalWidget
|
||||
embed: true
|
||||
|
||||
KisaniCalWidget:
|
||||
type: app-extension
|
||||
platform: iOS
|
||||
deploymentTarget: "16.0"
|
||||
sources:
|
||||
- path: KisaniCalWidget
|
||||
- path: Shared
|
||||
settings:
|
||||
base:
|
||||
SWIFT_VERSION: 5.9
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.kutesir.KisaniCal.widget
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
INFOPLIST_KEY_NSExtensionPointIdentifier: com.apple.widgetkit-extension
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
CODE_SIGN_ENTITLEMENTS: KisaniCalWidget/KisaniCalWidget.entitlements
|
||||
SKIP_INSTALL: YES
|
||||
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
|
||||
|
||||
Reference in New Issue
Block a user