Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6c195c3083
commit
9c5fe23488
@ -8,6 +8,10 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
EAC04AEE2F26BD5B007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC04AED2F26BD5B007F87EA /* Bedrock */; };
|
||||
EAC04D322F298D9B007F87EA /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAC04D312F298D9B007F87EA /* WidgetKit.framework */; };
|
||||
EAC04D342F298D9B007F87EA /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAC04D332F298D9B007F87EA /* SwiftUI.framework */; };
|
||||
EAC04D412F298D9C007F87EA /* AndromidaWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
EAC04E2C2F2998B2007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC04E2B2F2998B2007F87EA /* Bedrock */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -25,17 +29,67 @@
|
||||
remoteGlobalIDString = EAC04A972F26BAE8007F87EA;
|
||||
remoteInfo = Andromida;
|
||||
};
|
||||
EAC04D3F2F298D9C007F87EA /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = EAC04A902F26BAE8007F87EA /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = EAC04D2E2F298D9B007F87EA;
|
||||
remoteInfo = AndromidaWidgetExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
EAC04D462F298D9C007F87EA /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
EAC04D412F298D9C007F87EA /* AndromidaWidgetExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
EAC04A982F26BAE8007F87EA /* Andromida.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Andromida.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EAC04AA52F26BAE9007F87EA /* AndromidaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AndromidaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EAC04AAF2F26BAE9007F87EA /* AndromidaUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AndromidaUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AndromidaWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EAC04D312F298D9B007F87EA /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
EAC04D332F298D9B007F87EA /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
EAC04D4D2F298DD9007F87EA /* AndromidaWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AndromidaWidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
EAC04D422F298D9C007F87EA /* Exceptions for "AndromidaWidget" folder in "AndromidaWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */;
|
||||
};
|
||||
EAC04D512F298EAE007F87EA /* Exceptions for "Andromida" folder in "AndromidaWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
App/Models/ArcHabit.swift,
|
||||
App/Models/Ritual.swift,
|
||||
App/Models/RitualArc.swift,
|
||||
Assets.xcassets,
|
||||
Shared/Configuration/AppIdentifiers.swift,
|
||||
Shared/Services/RitualAnalytics.swift,
|
||||
);
|
||||
target = EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
EAC04A9A2F26BAE8007F87EA /* Andromida */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
EAC04D512F298EAE007F87EA /* Exceptions for "Andromida" folder in "AndromidaWidgetExtension" target */,
|
||||
);
|
||||
path = Andromida;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -49,6 +103,14 @@
|
||||
path = AndromidaUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EAC04D352F298D9B007F87EA /* AndromidaWidget */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
EAC04D422F298D9C007F87EA /* Exceptions for "AndromidaWidget" folder in "AndromidaWidgetExtension" target */,
|
||||
);
|
||||
path = AndromidaWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -74,15 +136,28 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
EAC04D2C2F298D9B007F87EA /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EAC04E2C2F2998B2007F87EA /* Bedrock in Frameworks */,
|
||||
EAC04D342F298D9B007F87EA /* SwiftUI.framework in Frameworks */,
|
||||
EAC04D322F298D9B007F87EA /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
EAC04A8F2F26BAE8007F87EA = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EAC04D4D2F298DD9007F87EA /* AndromidaWidgetExtension.entitlements */,
|
||||
EAC04A9A2F26BAE8007F87EA /* Andromida */,
|
||||
EAC04AA82F26BAE9007F87EA /* AndromidaTests */,
|
||||
EAC04AB22F26BAE9007F87EA /* AndromidaUITests */,
|
||||
EAC04D352F298D9B007F87EA /* AndromidaWidget */,
|
||||
EAC04D302F298D9B007F87EA /* Frameworks */,
|
||||
EAC04A992F26BAE8007F87EA /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@ -93,10 +168,20 @@
|
||||
EAC04A982F26BAE8007F87EA /* Andromida.app */,
|
||||
EAC04AA52F26BAE9007F87EA /* AndromidaTests.xctest */,
|
||||
EAC04AAF2F26BAE9007F87EA /* AndromidaUITests.xctest */,
|
||||
EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EAC04D302F298D9B007F87EA /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EAC04D312F298D9B007F87EA /* WidgetKit.framework */,
|
||||
EAC04D332F298D9B007F87EA /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -107,10 +192,12 @@
|
||||
EAC04A942F26BAE8007F87EA /* Sources */,
|
||||
EAC04A952F26BAE8007F87EA /* Frameworks */,
|
||||
EAC04A962F26BAE8007F87EA /* Resources */,
|
||||
EAC04D462F298D9C007F87EA /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
EAC04D402F298D9C007F87EA /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
EAC04A9A2F26BAE8007F87EA /* Andromida */,
|
||||
@ -169,6 +256,29 @@
|
||||
productReference = EAC04AAF2F26BAE9007F87EA /* AndromidaUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = EAC04D432F298D9C007F87EA /* Build configuration list for PBXNativeTarget "AndromidaWidgetExtension" */;
|
||||
buildPhases = (
|
||||
EAC04D2B2F298D9B007F87EA /* Sources */,
|
||||
EAC04D2C2F298D9B007F87EA /* Frameworks */,
|
||||
EAC04D2D2F298D9B007F87EA /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
EAC04D352F298D9B007F87EA /* AndromidaWidget */,
|
||||
);
|
||||
name = AndromidaWidgetExtension;
|
||||
packageProductDependencies = (
|
||||
EAC04E2B2F2998B2007F87EA /* Bedrock */,
|
||||
);
|
||||
productName = AndromidaWidgetExtension;
|
||||
productReference = EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@ -190,6 +300,9 @@
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
TestTargetID = EAC04A972F26BAE8007F87EA;
|
||||
};
|
||||
EAC04D2E2F298D9B007F87EA = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = EAC04A932F26BAE8007F87EA /* Build configuration list for PBXProject "Andromida" */;
|
||||
@ -212,6 +325,7 @@
|
||||
EAC04A972F26BAE8007F87EA /* Andromida */,
|
||||
EAC04AA42F26BAE9007F87EA /* AndromidaTests */,
|
||||
EAC04AAE2F26BAE9007F87EA /* AndromidaUITests */,
|
||||
EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -238,6 +352,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
EAC04D2D2F298D9B007F87EA /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -262,6 +383,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
EAC04D2B2F298D9B007F87EA /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
@ -275,11 +403,18 @@
|
||||
target = EAC04A972F26BAE8007F87EA /* Andromida */;
|
||||
targetProxy = EAC04AB02F26BAE9007F87EA /* PBXContainerItemProxy */;
|
||||
};
|
||||
EAC04D402F298D9C007F87EA /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */;
|
||||
targetProxy = EAC04D3F2F298D9C007F87EA /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
EAC04AB72F26BAE9007F87EA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@ -344,6 +479,8 @@
|
||||
};
|
||||
EAC04AB82F26BAE9007F87EA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@ -401,6 +538,8 @@
|
||||
};
|
||||
EAC04ABA2F26BAE9007F87EA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
@ -435,6 +574,8 @@
|
||||
};
|
||||
EAC04ABB2F26BAE9007F87EA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
@ -469,6 +610,8 @@
|
||||
};
|
||||
EAC04ABD2F26BAE9007F87EA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -476,7 +619,7 @@
|
||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@ -491,6 +634,8 @@
|
||||
};
|
||||
EAC04ABE2F26BAE9007F87EA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -498,7 +643,7 @@
|
||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@ -513,12 +658,14 @@
|
||||
};
|
||||
EAC04AC02F26BAE9007F87EA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@ -533,12 +680,14 @@
|
||||
};
|
||||
EAC04AC12F26BAE9007F87EA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@ -551,6 +700,72 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
EAC04D442F298D9C007F87EA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = AndromidaWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AndromidaWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AndromidaWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Andromida.AndromidaWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
EAC04D452F298D9C007F87EA /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */;
|
||||
baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = AndromidaWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AndromidaWidget/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AndromidaWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Andromida.AndromidaWidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@ -590,6 +805,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
EAC04D432F298D9C007F87EA /* Build configuration list for PBXNativeTarget "AndromidaWidgetExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
EAC04D442F298D9C007F87EA /* Debug */,
|
||||
EAC04D452F298D9C007F87EA /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
@ -604,6 +828,11 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Bedrock;
|
||||
};
|
||||
EAC04E2B2F2998B2007F87EA /* Bedrock */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EAC04AEC2F26BD5B007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */;
|
||||
productName = Bedrock;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = EAC04A902F26BAE8007F87EA /* Project object */;
|
||||
|
||||
@ -9,6 +9,11 @@
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>AndromidaWidgetExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -3,12 +3,16 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.mbrucedogs.Andromida</string>
|
||||
</array>
|
||||
<array>
|
||||
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -25,11 +25,15 @@ struct AndromidaApp: App {
|
||||
|
||||
// Include all models in schema - Ritual, RitualArc, and ArcHabit
|
||||
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
||||
|
||||
// Use App Group for shared container between app and widget
|
||||
let configuration = ModelConfiguration(
|
||||
schema: schema,
|
||||
isStoredInMemoryOnly: false,
|
||||
cloudKitDatabase: .private("iCloud.com.mbrucedogs.Andromida")
|
||||
url: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)?
|
||||
.appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite"),
|
||||
cloudKitDatabase: .private(AppIdentifiers.cloudKitContainerIdentifier)
|
||||
)
|
||||
|
||||
let container: ModelContainer
|
||||
do {
|
||||
container = try ModelContainer(for: schema, configurations: [configuration])
|
||||
|
||||
@ -1131,6 +1131,12 @@
|
||||
"comment" : "Habit title for a ritual preset focused on self-care, emphasizing gentle stretching as a habit.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Get a gentle nudge each morning to start your day right" : {
|
||||
"comment" : "Description for notification permission screen when user selected morning rituals."
|
||||
},
|
||||
"Get a gentle reminder when it's time for your rituals" : {
|
||||
"comment" : "Default description for notification permission screen."
|
||||
},
|
||||
"Get more done" : {
|
||||
"comment" : "Subtitle for the \"Focus\" onboarding goal.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1143,12 +1149,6 @@
|
||||
"comment" : "The text for the \"Get Started\" button in the welcome screen.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Get a gentle nudge each morning to start your day right" : {
|
||||
"comment" : "Description for notification permission screen when user selected morning rituals."
|
||||
},
|
||||
"Get a gentle reminder when it's time for your rituals" : {
|
||||
"comment" : "Default description for notification permission screen."
|
||||
},
|
||||
"Give your mind a break from screens." : {
|
||||
"comment" : "Notes for a ritual preset focused on giving the mind a break from screens.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1367,6 +1367,18 @@
|
||||
"comment" : "Habit title for a mindfulness ritual where the user writes in a journal for 5 minutes.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Keep going" : {
|
||||
"comment" : "Cancel button label to continue onboarding.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Keep going"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Keep room cool" : {
|
||||
"comment" : "Habit title for keeping the bedroom cool at night.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1456,29 +1468,6 @@
|
||||
},
|
||||
"Mindfulness" : {
|
||||
|
||||
},
|
||||
"Momentum at a glance" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Momentum at a glance"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Impulso de un vistazo"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Élan en un coup d’œil"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"More" : {
|
||||
"comment" : "The text for a button that expands or collapses a list.",
|
||||
@ -2169,22 +2158,6 @@
|
||||
},
|
||||
"Shows rituals for the current time of day. Check in here daily." : {
|
||||
|
||||
},
|
||||
"Skip for now" : {
|
||||
"comment" : "A button label that allows users to skip creating a new ritual for now.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Skip setup?" : {
|
||||
"comment" : "Alert title asking if the user wants to skip onboarding setup.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Skip setup?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skip" : {
|
||||
"comment" : "Button label to skip onboarding.",
|
||||
@ -2198,26 +2171,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Keep going" : {
|
||||
"comment" : "Cancel button label to continue onboarding.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Keep going"
|
||||
}
|
||||
}
|
||||
}
|
||||
"Skip for now" : {
|
||||
"comment" : "A button label that allows users to skip creating a new ritual for now.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"You can complete setup later in Settings." : {
|
||||
"comment" : "Alert message explaining that setup can be completed later in Settings.",
|
||||
"Skip setup?" : {
|
||||
"comment" : "Alert title asking if the user wants to skip onboarding setup.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "You can complete setup later in Settings."
|
||||
"value" : "Skip setup?"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2282,9 +2247,6 @@
|
||||
"comment" : "Description of a trend direction when there is no significant change.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Stay on track" : {
|
||||
"comment" : "Headline on the notification permission screen during onboarding."
|
||||
},
|
||||
"Start" : {
|
||||
"comment" : "A button that starts a new arc for a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2324,6 +2286,9 @@
|
||||
"comment" : "Subtitle for the \"Morning\" option in the \"Time of Day\" section of the onboarding screen.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Stay on track" : {
|
||||
"comment" : "Headline on the notification permission screen during onboarding."
|
||||
},
|
||||
"Streak" : {
|
||||
"comment" : "Title of a section in the Insight Cards view, related to streaks of consecutive perfect days.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2358,29 +2323,6 @@
|
||||
"comment" : "Description of a ritual preset that serves as a weekly reset to help users start their week on a positive note.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Switch tabs to explore rituals and insights" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Switch tabs to explore rituals and insights"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cambia de pestaña para explorar rituales y perspectivas"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Passez d’un onglet à l’autre pour explorer les rituels et les aperçus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Take 5 deep breaths" : {
|
||||
"comment" : "Habit title for a ritual preset focused on productivity, encouraging the user to take deep breaths.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2608,6 +2550,9 @@
|
||||
"comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to manage all your rituals, regardless of the time they were created.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"We'll remind you at the perfect times for your morning and evening rituals" : {
|
||||
"comment" : "Description for notification permission screen when user selected both morning and evening rituals."
|
||||
},
|
||||
"Weekly average" : {
|
||||
"comment" : "Caption for the 7-Day Avg insight card."
|
||||
},
|
||||
@ -2623,9 +2568,6 @@
|
||||
"comment" : "The title of the welcome screen in the setup wizard.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"We'll remind you at the perfect times for your morning and evening rituals" : {
|
||||
"comment" : "Description for notification permission screen when user selected both morning and evening rituals."
|
||||
},
|
||||
"Wellness" : {
|
||||
"comment" : "The category of the morning ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2676,13 +2618,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Wind down with a reminder when it's time for your evening ritual" : {
|
||||
"comment" : "Description for notification permission screen when user selected evening rituals."
|
||||
},
|
||||
"Wind down with habits that promote quality sleep." : {
|
||||
"comment" : "Notes section of a ritual preset focused on sleep preparation.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Wind down with a reminder when it's time for your evening ritual" : {
|
||||
"comment" : "Description for notification permission screen when user selected evening rituals."
|
||||
},
|
||||
"Wind down with intention" : {
|
||||
"comment" : "Subtitle for the \"Evening\" section of the \"Both\" time preference option in the onboarding flow.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2721,6 +2663,18 @@
|
||||
"comment" : "A hint displayed below the \"Browse Presets\" button, encouraging users to explore preset rituals.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"You can complete setup later in Settings." : {
|
||||
"comment" : "Alert message explaining that setup can be completed later in Settings.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "You can complete setup later in Settings."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"You completed your first check-in" : {
|
||||
"comment" : "A description of the positive outcome of completing a first check-in.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2752,29 +2706,6 @@
|
||||
"comment" : "The title of the preview step in the ritual creation flow.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Your focus ritual lives here" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Your focus ritual lives here"
|
||||
}
|
||||
},
|
||||
"es-MX" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Tu ritual de enfoque vive aquí"
|
||||
}
|
||||
},
|
||||
"fr-CA" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Votre rituel principal se trouve ici"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Your highest-performing ritual by completion rate in the current arc. Keep it up!" : {
|
||||
"comment" : "Explanation for the Best Ritual insight card."
|
||||
},
|
||||
|
||||
@ -66,6 +66,18 @@ enum TimeOfDay: String, Codable, CaseIterable, Comparable {
|
||||
static func < (lhs: TimeOfDay, rhs: TimeOfDay) -> Bool {
|
||||
lhs.sortOrder < rhs.sortOrder
|
||||
}
|
||||
|
||||
/// Returns the current time period based on the hour of the day.
|
||||
static func current(for date: Date = Date()) -> TimeOfDay {
|
||||
let hour = Calendar.current.component(.hour, from: date)
|
||||
switch hour {
|
||||
case 0..<11: return .morning
|
||||
case 11..<14: return .midday
|
||||
case 14..<17: return .afternoon
|
||||
case 17..<21: return .evening
|
||||
default: return .night
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A ritual represents a persistent habit-building journey. It contains multiple
|
||||
|
||||
@ -3,6 +3,7 @@ import Observation
|
||||
import SwiftData
|
||||
import CoreData
|
||||
import Bedrock
|
||||
import WidgetKit
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
@ -69,8 +70,12 @@ final class RitualStore: RitualStoreProviding {
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.reloadRituals()
|
||||
// Hop to the main actor and capture a strong reference safely there
|
||||
Task { @MainActor [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.reloadRituals()
|
||||
// Also refresh widgets when data arrives from other devices
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,16 +171,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
/// Filters based on time of day: morning (before 11am), midday (11am-2pm), afternoon (2pm-5pm),
|
||||
/// evening (5pm-9pm), night (after 9pm). Anytime rituals are always shown.
|
||||
func ritualsForToday() -> [Ritual] {
|
||||
let hour = calendar.component(.hour, from: Date())
|
||||
let currentPeriod: TimeOfDay = {
|
||||
switch hour {
|
||||
case 0..<11: return .morning
|
||||
case 11..<14: return .midday
|
||||
case 14..<17: return .afternoon
|
||||
case 17..<21: return .evening
|
||||
default: return .night
|
||||
}
|
||||
}()
|
||||
let currentPeriod = TimeOfDay.current()
|
||||
|
||||
return currentRituals.filter { ritual in
|
||||
guard let arc = ritual.currentArc, arc.contains(date: Date()) else { return false }
|
||||
@ -831,6 +827,8 @@ final class RitualStore: RitualStoreProviding {
|
||||
do {
|
||||
try modelContext.save()
|
||||
reloadRituals()
|
||||
// Notify widgets that data has changed
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
} catch {
|
||||
lastErrorMessage = error.localizedDescription
|
||||
}
|
||||
@ -911,7 +909,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
}
|
||||
|
||||
private func dayIdentifier(for date: Date) -> String {
|
||||
dayFormatter.string(from: date)
|
||||
RitualAnalytics.dayIdentifier(for: date)
|
||||
}
|
||||
|
||||
// MARK: - History / Calendar Support
|
||||
|
||||
@ -96,8 +96,10 @@ final class SettingsStore: CloudSyncable, ThemeProviding {
|
||||
|
||||
private func observeCloudChanges() {
|
||||
cloudSync.onCloudDataReceived = { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.handleCloudDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
cloudChangeObserver = NotificationCenter.default.addObserver(
|
||||
forName: .persistedDataDidChange,
|
||||
@ -109,9 +111,11 @@ final class SettingsStore: CloudSyncable, ThemeProviding {
|
||||
identifier == AppSettingsData.dataIdentifier else {
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
self.handleCloudDataChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCloudDataChange() {
|
||||
refreshSettingsData()
|
||||
|
||||
@ -85,6 +85,26 @@ struct RootView: View {
|
||||
store.reminderScheduler.shouldNavigateToToday = false
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handleURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleURL(_ url: URL) {
|
||||
guard url.scheme == "andromida" else { return }
|
||||
|
||||
switch url.host {
|
||||
case "today":
|
||||
selectedTab = .today
|
||||
case "rituals":
|
||||
selectedTab = .rituals
|
||||
case "insights":
|
||||
selectedTab = .insights
|
||||
case "history":
|
||||
selectedTab = .history
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshCurrentTab() {
|
||||
|
||||
@ -6,14 +6,7 @@ struct TodayNoRitualsForTimeView: View {
|
||||
@Bindable var store: RitualStore
|
||||
|
||||
private var currentTimePeriod: TimeOfDay {
|
||||
let hour = Calendar.current.component(.hour, from: Date())
|
||||
switch hour {
|
||||
case 0..<11: return .morning
|
||||
case 11..<14: return .midday
|
||||
case 14..<17: return .afternoon
|
||||
case 17..<21: return .evening
|
||||
default: return .night
|
||||
}
|
||||
TimeOfDay.current()
|
||||
}
|
||||
|
||||
private var nextRituals: [Ritual] {
|
||||
|
||||
23
Andromida/Shared/Configuration/AppIdentifiers.swift
Normal file
23
Andromida/Shared/Configuration/AppIdentifiers.swift
Normal file
@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
enum AppIdentifiers {
|
||||
// Read from Info.plist (values come from xcconfig)
|
||||
static let appGroupIdentifier: String = {
|
||||
Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String
|
||||
?? "group.com.mbrucedogs.Andromida"
|
||||
}()
|
||||
|
||||
static let cloudKitContainerIdentifier: String = {
|
||||
Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String
|
||||
?? "iCloud.com.mbrucedogs.Andromida"
|
||||
}()
|
||||
|
||||
// Derived from bundle identifier
|
||||
static var bundleIdentifier: String {
|
||||
Bundle.main.bundleIdentifier ?? "com.mbrucedogs.Andromida"
|
||||
}
|
||||
|
||||
static var widgetBundleIdentifier: String {
|
||||
"\(bundleIdentifier).Widget"
|
||||
}
|
||||
}
|
||||
25
Andromida/Shared/Configuration/Base.xcconfig
Normal file
25
Andromida/Shared/Configuration/Base.xcconfig
Normal file
@ -0,0 +1,25 @@
|
||||
// Base.xcconfig - Source of truth for all identifiers
|
||||
// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below
|
||||
|
||||
// =============================================================================
|
||||
// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION
|
||||
// =============================================================================
|
||||
|
||||
COMPANY_IDENTIFIER = com.mbrucedogs
|
||||
APP_NAME = Andromida
|
||||
DEVELOPMENT_TEAM = // Add your Team ID here if needed
|
||||
|
||||
// =============================================================================
|
||||
// DERIVED IDENTIFIERS - DO NOT EDIT
|
||||
// =============================================================================
|
||||
|
||||
APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)
|
||||
WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp
|
||||
APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip
|
||||
WIDGET_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Widget
|
||||
INTENT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Intent
|
||||
TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests
|
||||
UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests
|
||||
|
||||
APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME)
|
||||
CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME)
|
||||
6
Andromida/Shared/Configuration/Debug.xcconfig
Normal file
6
Andromida/Shared/Configuration/Debug.xcconfig
Normal file
@ -0,0 +1,6 @@
|
||||
// Debug.xcconfig
|
||||
#include "Base.xcconfig"
|
||||
|
||||
// Debug-specific settings
|
||||
SWIFT_OPTIMIZATION_LEVEL = -Onone
|
||||
ENABLE_TESTABILITY = YES
|
||||
6
Andromida/Shared/Configuration/Release.xcconfig
Normal file
6
Andromida/Shared/Configuration/Release.xcconfig
Normal file
@ -0,0 +1,6 @@
|
||||
// Release.xcconfig
|
||||
#include "Base.xcconfig"
|
||||
|
||||
// Release-specific settings
|
||||
SWIFT_OPTIMIZATION_LEVEL = -O
|
||||
SWIFT_COMPILATION_MODE = wholemodule
|
||||
83
Andromida/Shared/Services/RitualAnalytics.swift
Normal file
83
Andromida/Shared/Services/RitualAnalytics.swift
Normal file
@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
|
||||
/// Shared logic for ritual analytics and data processing used by both the app and widgets.
|
||||
enum RitualAnalytics {
|
||||
/// Returns a unique string identifier for a given date (YYYY-MM-DD).
|
||||
static func dayIdentifier(for date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
/// Calculates the current streak of consecutive days with activity.
|
||||
/// - Parameters:
|
||||
/// - rituals: The list of rituals to analyze.
|
||||
/// - calendar: The calendar to use for date calculations.
|
||||
/// - Returns: The current streak count.
|
||||
static func calculateCurrentStreak(rituals: [Ritual], calendar: Calendar = .current) -> Int {
|
||||
var allCompletions = Set<String>()
|
||||
|
||||
// Collect all completed day IDs from all habits across all rituals
|
||||
for ritual in rituals {
|
||||
for arc in ritual.arcs ?? [] {
|
||||
for habit in arc.habits ?? [] {
|
||||
for dID in habit.completedDayIDs {
|
||||
allCompletions.insert(dID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allCompletions.isEmpty { return 0 }
|
||||
|
||||
// Count backwards from today
|
||||
var streak = 0
|
||||
var checkDate = calendar.startOfDay(for: Date())
|
||||
|
||||
// If today isn't perfect, check if yesterday was to maintain streak
|
||||
var currentDayID = dayIdentifier(for: checkDate)
|
||||
if !allCompletions.contains(currentDayID) {
|
||||
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
|
||||
currentDayID = dayIdentifier(for: checkDate)
|
||||
}
|
||||
|
||||
while allCompletions.contains(currentDayID) {
|
||||
streak += 1
|
||||
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
|
||||
currentDayID = dayIdentifier(for: checkDate)
|
||||
if streak > 3650 { break } // Safety cap (10 years)
|
||||
}
|
||||
|
||||
return streak
|
||||
}
|
||||
|
||||
/// Filters rituals that are active on a specific date and match the current time of day.
|
||||
static func ritualsActive(on date: Date, from rituals: [Ritual]) -> [Ritual] {
|
||||
let timeOfDay = TimeOfDay.current(for: date)
|
||||
|
||||
return rituals.filter { ritual in
|
||||
guard ritual.arcs?.first(where: { $0.isActive && $0.contains(date: date) }) != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
return ritual.timeOfDay == .anytime || ritual.timeOfDay == timeOfDay
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the overall completion rate for all rituals active on a specific date.
|
||||
static func overallCompletionRate(on date: Date, from rituals: [Ritual]) -> Double {
|
||||
let dayID = dayIdentifier(for: date)
|
||||
var allTodayHabits: [ArcHabit] = []
|
||||
|
||||
for ritual in rituals {
|
||||
if let arc = ritual.arcs?.first(where: { $0.isActive && $0.contains(date: date) }) {
|
||||
allTodayHabits.append(contentsOf: arc.habits ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
guard !allTodayHabits.isEmpty else { return 0.0 }
|
||||
|
||||
let completedCount = allTodayHabits.filter { $0.completedDayIDs.contains(dayID) }.count
|
||||
return Double(completedCount) / Double(allTodayHabits.count)
|
||||
}
|
||||
}
|
||||
10
AndromidaWidget/AndromidaWidget.entitlements
Normal file
10
AndromidaWidget/AndromidaWidget.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>$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
17
AndromidaWidget/AndromidaWidget.swift
Normal file
17
AndromidaWidget/AndromidaWidget.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct AndromidaWidget: Widget {
|
||||
let kind: String = "AndromidaWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: AndromidaWidgetProvider()) { entry in
|
||||
AndromidaWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Andromida")
|
||||
.description("Track your daily rituals and habits.")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
||||
.contentMarginsDisabled()
|
||||
}
|
||||
}
|
||||
15
AndromidaWidget/Info.plist
Normal file
15
AndromidaWidget/Info.plist
Normal file
@ -0,0 +1,15 @@
|
||||
<?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>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>AppGroupIdentifier</key>
|
||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||
<key>CloudKitContainerIdentifier</key>
|
||||
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
9
AndromidaWidget/Intents/ConfigurationAppIntent.swift
Normal file
9
AndromidaWidget/Intents/ConfigurationAppIntent.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import WidgetKit
|
||||
import AppIntents
|
||||
|
||||
struct ConfigurationAppIntent: WidgetConfigurationIntent {
|
||||
static var title: LocalizedStringResource { "Andromida" }
|
||||
static var description: IntentDescription { "Track your daily rituals and habits." }
|
||||
|
||||
// We can add more parameters here later, like selecting a specific ritual.
|
||||
}
|
||||
9
AndromidaWidget/Models/HabitEntry.swift
Normal file
9
AndromidaWidget/Models/HabitEntry.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
struct HabitEntry: Identifiable {
|
||||
let id: UUID
|
||||
let title: String
|
||||
let symbolName: String
|
||||
let ritualTitle: String
|
||||
let isCompleted: Bool
|
||||
}
|
||||
14
AndromidaWidget/Models/WidgetEntry.swift
Normal file
14
AndromidaWidget/Models/WidgetEntry.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import WidgetKit
|
||||
import Foundation
|
||||
|
||||
struct WidgetEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let configuration: ConfigurationAppIntent
|
||||
let completionRate: Double
|
||||
let currentStreak: Int
|
||||
let nextHabits: [HabitEntry]
|
||||
let weeklyTrend: [Double]
|
||||
let currentTimeOfDay: String
|
||||
let currentTimeOfDaySymbol: String
|
||||
let currentTimeOfDayRange: String
|
||||
}
|
||||
114
AndromidaWidget/Providers/AndromidaWidgetProvider.swift
Normal file
114
AndromidaWidget/Providers/AndromidaWidgetProvider.swift
Normal file
@ -0,0 +1,114 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import AppIntents
|
||||
|
||||
struct AndromidaWidgetProvider: AppIntentTimelineProvider {
|
||||
func placeholder(in context: Context) -> WidgetEntry {
|
||||
WidgetEntry(
|
||||
date: Date(),
|
||||
configuration: ConfigurationAppIntent(),
|
||||
completionRate: 0.75,
|
||||
currentStreak: 5,
|
||||
nextHabits: [
|
||||
HabitEntry(id: UUID(), title: "Morning Meditation", symbolName: "figure.mind.and.body", ritualTitle: "Mindfulness", isCompleted: false),
|
||||
HabitEntry(id: UUID(), title: "Drink Water", symbolName: "drop.fill", ritualTitle: "Health", isCompleted: true)
|
||||
],
|
||||
weeklyTrend: [0.5, 0.7, 0.6, 0.9, 0.8, 0.75, 0.0],
|
||||
currentTimeOfDay: "Morning",
|
||||
currentTimeOfDaySymbol: "sunrise.fill",
|
||||
currentTimeOfDayRange: "Before 11am"
|
||||
)
|
||||
}
|
||||
|
||||
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> WidgetEntry {
|
||||
await fetchLatestData(for: configuration)
|
||||
}
|
||||
|
||||
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<WidgetEntry> {
|
||||
let entry = await fetchLatestData(for: configuration)
|
||||
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date()
|
||||
return Timeline(entries: [entry], policy: .after(nextUpdate))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func fetchLatestData(for configuration: ConfigurationAppIntent) -> WidgetEntry {
|
||||
let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self])
|
||||
let configurationURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)?
|
||||
.appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite")
|
||||
|
||||
let modelConfig = ModelConfiguration(schema: schema, url: configurationURL)
|
||||
|
||||
do {
|
||||
let container = try ModelContainer(for: schema, configurations: [modelConfig])
|
||||
let context = container.mainContext
|
||||
|
||||
let descriptor = FetchDescriptor<Ritual>()
|
||||
let rituals = try context.fetch(descriptor)
|
||||
|
||||
let today = Date()
|
||||
let dayID = RitualAnalytics.dayIdentifier(for: today)
|
||||
let timeOfDay = TimeOfDay.current(for: today)
|
||||
|
||||
// Match the app's logic for "Today" view
|
||||
let todayRituals = RitualAnalytics.ritualsActive(on: today, from: rituals)
|
||||
.sorted { lhs, rhs in
|
||||
if lhs.timeOfDay != rhs.timeOfDay {
|
||||
return lhs.timeOfDay < rhs.timeOfDay
|
||||
}
|
||||
return lhs.sortIndex < rhs.sortIndex
|
||||
}
|
||||
|
||||
var visibleHabits: [HabitEntry] = []
|
||||
for ritual in todayRituals {
|
||||
if let arc = ritual.arcs?.first(where: { $0.isActive && $0.contains(date: today) }) {
|
||||
// Sort habits within each ritual by their sortIndex
|
||||
let sortedHabits = (arc.habits ?? []).sorted { $0.sortIndex < $1.sortIndex }
|
||||
for habit in sortedHabits {
|
||||
visibleHabits.append(HabitEntry(
|
||||
id: habit.id,
|
||||
title: habit.title,
|
||||
symbolName: habit.symbolName,
|
||||
ritualTitle: ritual.title,
|
||||
isCompleted: habit.completedDayIDs.contains(dayID)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall progress across ALL rituals for today
|
||||
let overallRate = RitualAnalytics.overallCompletionRate(on: today, from: rituals)
|
||||
|
||||
// Next habits (limit to 4) - still filtered by current time of day for the list
|
||||
let nextHabits = visibleHabits.prefix(4)
|
||||
|
||||
// Streak calculation
|
||||
let streak = RitualAnalytics.calculateCurrentStreak(rituals: rituals)
|
||||
|
||||
return WidgetEntry(
|
||||
date: today,
|
||||
configuration: configuration,
|
||||
completionRate: overallRate,
|
||||
currentStreak: streak,
|
||||
nextHabits: Array(nextHabits),
|
||||
weeklyTrend: [],
|
||||
currentTimeOfDay: timeOfDay.displayName,
|
||||
currentTimeOfDaySymbol: timeOfDay.symbolName,
|
||||
currentTimeOfDayRange: timeOfDay.timeRange
|
||||
)
|
||||
} catch {
|
||||
// Return a default entry instead of placeholder(in: .preview)
|
||||
return WidgetEntry(
|
||||
date: Date(),
|
||||
configuration: configuration,
|
||||
completionRate: 0.0,
|
||||
currentStreak: 0,
|
||||
nextHabits: [],
|
||||
weeklyTrend: [],
|
||||
currentTimeOfDay: "Today",
|
||||
currentTimeOfDaySymbol: "clock.fill",
|
||||
currentTimeOfDayRange: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
AndromidaWidget/Resources/Assets.xcassets/Contents.json
Normal file
6
AndromidaWidget/Resources/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
30
AndromidaWidget/Views/AndromidaWidgetView.swift
Normal file
30
AndromidaWidget/Views/AndromidaWidgetView.swift
Normal file
@ -0,0 +1,30 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct AndromidaWidgetView: View {
|
||||
var entry: WidgetEntry
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
var body: some View {
|
||||
switch family {
|
||||
case .systemSmall:
|
||||
SmallWidgetView(entry: entry)
|
||||
.widgetURL(URL(string: "andromida://today"))
|
||||
case .systemMedium:
|
||||
MediumWidgetView(entry: entry)
|
||||
.widgetURL(URL(string: "andromida://today"))
|
||||
case .systemLarge:
|
||||
LargeWidgetView(entry: entry)
|
||||
.widgetURL(URL(string: "andromida://today"))
|
||||
default:
|
||||
SmallWidgetView(entry: entry)
|
||||
.widgetURL(URL(string: "andromida://today"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Branding Colors Helper
|
||||
extension Color {
|
||||
static let brandingPrimary = Color(red: 0.12, green: 0.09, blue: 0.08)
|
||||
static let brandingAccent = Color(red: 0.95, green: 0.60, blue: 0.45) // Matches the orange-ish accent in your app
|
||||
}
|
||||
78
AndromidaWidget/Views/Components/LargeWidgetView.swift
Normal file
78
AndromidaWidget/Views/Components/LargeWidgetView.swift
Normal file
@ -0,0 +1,78 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import Bedrock
|
||||
|
||||
struct LargeWidgetView: View {
|
||||
let entry: WidgetEntry
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
Text(String(localized: "Today's Progress"))
|
||||
.styled(.heading, emphasis: .custom(.white))
|
||||
Text("\(entry.currentStreak) day streak")
|
||||
.styled(.subheading, emphasis: .custom(Color.brandingAccent))
|
||||
}
|
||||
Spacer()
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 6)
|
||||
Circle()
|
||||
.trim(from: 0, to: entry.completionRate)
|
||||
.stroke(Color.brandingAccent, style: StrokeStyle(lineWidth: 6, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
Text("\(Int(entry.completionRate * 100))%")
|
||||
.styled(.captionEmphasis, emphasis: .custom(.white))
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.background(Color.white.opacity(0.2))
|
||||
|
||||
if entry.nextHabits.isEmpty {
|
||||
Spacer()
|
||||
WidgetEmptyStateView(
|
||||
title: String(localized: "No rituals scheduled for \(entry.currentTimeOfDay.lowercased())."),
|
||||
subtitle: entry.currentTimeOfDay,
|
||||
symbolName: entry.currentTimeOfDaySymbol,
|
||||
timeRange: entry.currentTimeOfDayRange
|
||||
)
|
||||
Spacer()
|
||||
} else {
|
||||
Text(String(localized: "Habits"))
|
||||
.styled(.captionEmphasis, emphasis: .custom(.white.opacity(0.7)))
|
||||
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
ForEach(entry.nextHabits) { habit in
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
Image(systemName: habit.symbolName)
|
||||
.foregroundColor(Color.brandingAccent)
|
||||
.font(.system(size: 18))
|
||||
.frame(width: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
||||
Text(habit.title)
|
||||
.styled(.subheading, emphasis: .custom(.white))
|
||||
Text(habit.ritualTitle)
|
||||
.styled(.caption, emphasis: .custom(.white.opacity(0.5)))
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Image(systemName: habit.isCompleted ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundColor(habit.isCompleted ? .green : .white.opacity(0.2))
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(Design.Spacing.large)
|
||||
.containerBackground(for: .widget) {
|
||||
Color.brandingPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
79
AndromidaWidget/Views/Components/MediumWidgetView.swift
Normal file
79
AndromidaWidget/Views/Components/MediumWidgetView.swift
Normal file
@ -0,0 +1,79 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import Bedrock
|
||||
|
||||
struct MediumWidgetView: View {
|
||||
let entry: WidgetEntry
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Left side: Progress & Streak
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 8)
|
||||
Circle()
|
||||
.trim(from: 0, to: entry.completionRate)
|
||||
.stroke(Color.brandingAccent, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Text("\(Int(entry.completionRate * 100))%")
|
||||
.styled(.heading, emphasis: .custom(.white))
|
||||
Text(String(localized: "Today"))
|
||||
.styled(.caption, emphasis: .custom(.white.opacity(0.7)))
|
||||
}
|
||||
}
|
||||
.frame(width: 72, height: 72)
|
||||
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(Color.brandingAccent)
|
||||
.font(.system(size: 14))
|
||||
Text("\(entry.currentStreak)d")
|
||||
.styled(.captionEmphasis, emphasis: .custom(.white))
|
||||
}
|
||||
}
|
||||
.frame(width: 110)
|
||||
|
||||
// Right side: Habits
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
if entry.nextHabits.isEmpty {
|
||||
WidgetEmptyStateView(
|
||||
title: String(localized: "No rituals now"),
|
||||
subtitle: entry.currentTimeOfDay,
|
||||
symbolName: entry.currentTimeOfDaySymbol,
|
||||
timeRange: entry.currentTimeOfDayRange
|
||||
)
|
||||
} else {
|
||||
Text(String(localized: "Next Habits"))
|
||||
.styled(.captionEmphasis, emphasis: .custom(.white.opacity(0.7)))
|
||||
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
ForEach(entry.nextHabits.prefix(3)) { habit in
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
Image(systemName: habit.isCompleted ? "checkmark.circle.fill" : habit.symbolName)
|
||||
.foregroundColor(habit.isCompleted ? .green : Color.brandingAccent)
|
||||
.font(.system(size: 14))
|
||||
.frame(width: 20)
|
||||
|
||||
Text(habit.title)
|
||||
.styled(.subheading, emphasis: .custom(.white))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.large)
|
||||
.padding(.trailing, Design.Spacing.medium)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.containerBackground(for: .widget) {
|
||||
Color.brandingPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
38
AndromidaWidget/Views/Components/SmallWidgetView.swift
Normal file
38
AndromidaWidget/Views/Components/SmallWidgetView.swift
Normal file
@ -0,0 +1,38 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import Bedrock
|
||||
|
||||
struct SmallWidgetView: View {
|
||||
let entry: WidgetEntry
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.1), lineWidth: 8)
|
||||
Circle()
|
||||
.trim(from: 0, to: entry.completionRate)
|
||||
.stroke(Color.brandingAccent, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Text("\(Int(entry.completionRate * 100))%")
|
||||
.styled(.heading, emphasis: .custom(.white))
|
||||
Text(String(localized: "Today"))
|
||||
.styled(.caption, emphasis: .custom(.white.opacity(0.7)))
|
||||
}
|
||||
}
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(Color.brandingAccent)
|
||||
Text("\(entry.currentStreak) day streak")
|
||||
.styled(.captionEmphasis, emphasis: .custom(.white))
|
||||
}
|
||||
}
|
||||
.containerBackground(for: .widget) {
|
||||
Color.brandingPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
31
AndromidaWidget/Views/Components/WidgetEmptyStateView.swift
Normal file
31
AndromidaWidget/Views/Components/WidgetEmptyStateView.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
struct WidgetEmptyStateView: View {
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let symbolName: String
|
||||
let timeRange: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.small) {
|
||||
SymbolIcon(symbolName, size: .hero, color: Color.brandingAccent.opacity(0.6))
|
||||
|
||||
VStack(spacing: Design.Spacing.xSmall) {
|
||||
Text(title)
|
||||
.styled(.subheading, emphasis: .custom(.white))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if !timeRange.isEmpty {
|
||||
Text(timeRange)
|
||||
.styled(.caption, emphasis: .custom(.white.opacity(0.5)))
|
||||
}
|
||||
}
|
||||
|
||||
Text(String(localized: "Enjoy this moment."))
|
||||
.styled(.caption, emphasis: .custom(.white.opacity(0.5)))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
10
AndromidaWidgetExtension.entitlements
Normal file
10
AndromidaWidgetExtension.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>$(APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Loading…
Reference in New Issue
Block a user