Update signing configuration to use 88oakapps.feels identifiers
- Update App Group IDs from group.com.tt.feels to group.com.88oakapps.feels - Update iCloud container IDs from iCloud.com.tt.feels to iCloud.com.88oakapps.feels - Sync code constants with entitlements across all targets (iOS, Watch, Widget) - Update documentation in CLAUDE.md and PROJECT_OVERVIEW.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,10 @@
|
||||
"Bash(unzip:*)",
|
||||
"Bash(plutil:*)",
|
||||
"Bash(done)",
|
||||
"Bash(for:*)"
|
||||
"Bash(for:*)",
|
||||
"Bash(# Check Button and Label strings grep -rE ''\\(Button|Label|navigationTitle\\)\\\\\\(\"\"'' /Users/treyt/Desktop/code/Feels/Shared/ --include=\"\"*.swift\"\")",
|
||||
"Bash(# Double-check a few of these strings to make sure they''re not used echo \"\"=== Checking ''Custom'' ===\"\" grep -rn ''\"\"Custom\"\"'' /Users/treyt/Desktop/code/Feels/Shared/ --include=\"\"*.swift\"\")",
|
||||
"Bash(echo \"=== How ''3D card flip'' is used ===\" grep -rn \"3D card flip\" /Users/treyt/Desktop/code/Feels/Shared/ --include=\"*.swift\")"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(git commit:*)",
|
||||
|
||||
@@ -30,8 +30,8 @@ Core Data operations are split across files in `Shared/Persistence/`:
|
||||
|
||||
## App Groups
|
||||
|
||||
- **Production**: `group.com.88oakapps.ifeel`
|
||||
- **Debug**: `group.com.88oakapps.ifeelDebug`
|
||||
- **Production**: `group.com.88oakapps.feels`
|
||||
- **Debug**: `group.com.88oakapps.feels.debug`
|
||||
|
||||
## Build & Run
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.access</key>
|
||||
<array>
|
||||
<string>health-records</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.tt.feelsDebug</string>
|
||||
<string>iCloud.com.88oakapps.feels</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
@@ -12,13 +20,7 @@
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.feelsDebug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.access</key>
|
||||
<array>
|
||||
<string>health-records</string>
|
||||
<string>group.com.88oakapps.feels</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.access</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.tt.feelsDebug</string>
|
||||
<string>iCloud.com.88oakapps.feels.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
@@ -14,11 +18,7 @@
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.feelsDebug</string>
|
||||
<string>group.com.88oakapps.feels.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.healthkit.access</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
<!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>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>group.com.tt.feels</string>
|
||||
<string>iCloud.com.88oakapps.feels</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>iCloud.com.tt.feels</string>
|
||||
<string>group.com.88oakapps.feels</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
<!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>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>group.com.tt.feelsDebug</string>
|
||||
<string>iCloud.com.88oakapps.feels.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>iCloud.com.tt.feelsDebug</string>
|
||||
<string>group.com.88oakapps.feels.debug</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -23,17 +23,5 @@
|
||||
<string>processing</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>Feels uses your health data to find correlations between your activity, sleep, and mood patterns to provide personalized insights.</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
<string>Feels syncs your mood data to Apple Health so you can see how your emotions correlate with sleep, exercise, and other health metrics.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Feels uses the camera to take photos for your mood journal entries.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Feels accesses your photo library to attach photos to your mood journal entries.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Feels uses Face ID to protect your private mood data.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -578,6 +578,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Feels Watch App/Feels Watch AppDebug.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = QND55P4443;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Feels;
|
||||
@@ -589,7 +590,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug.watchkitapp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.watch.debug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -733,10 +734,17 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = QND55P4443;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Feels--iOS--Info.plist";
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Feels uses the camera to take photos for your mood journal entries.";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Feels uses Face ID to protect your private mood data.";
|
||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "Feels uses your health data to find correlations between your activity, sleep, and mood patterns to provide personalized insights.";
|
||||
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Feels syncs your mood data to Apple Health so you can see how your emotions correlate with sleep, exercise, and other health metrics.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Feels accesses your photo library to attach photos to your mood journal entries.";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -748,7 +756,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.debug;
|
||||
PRODUCT_NAME = Feels;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -768,10 +776,17 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = QND55P4443;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Feels--iOS--Info.plist";
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Feels uses the camera to take photos for your mood journal entries.";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Feels uses Face ID to protect your private mood data.";
|
||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "Feels uses your health data to find correlations between your activity, sleep, and mood patterns to provide personalized insights.";
|
||||
INFOPLIST_KEY_NSHealthUpdateUsageDescription = "Feels syncs your mood data to Apple Health so you can see how your emotions correlate with sleep, exercise, and other health metrics.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Feels accesses your photo library to attach photos to your mood journal entries.";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -783,7 +798,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels;
|
||||
PRODUCT_NAME = Feels;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -938,10 +953,12 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = QND55P4443;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "FeelsWidgetExtension-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = FeelsWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -949,7 +966,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug.FeelsWidgetDebug;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.widget.debug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -970,10 +987,12 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = QND55P4443;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "FeelsWidgetExtension-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = FeelsWidget;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -981,7 +1000,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug.FeelsWidget;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.widget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1002,18 +1021,19 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Feels Watch App/Feels Watch App.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
DEVELOPMENT_TEAM = QND55P4443;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Feels;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.tt.feels;
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.tt.feelsDebug;
|
||||
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feels.watchkitapp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.watch;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<key>Feels (macOS).xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>Feels Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>FeelsWidgetExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,46 +32,49 @@ struct VotingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Small Widget: 3 over 2 grid (no text - just mood buttons)
|
||||
// MARK: - Small Widget: 3 over 2 grid centered in 50%|50% vertical split
|
||||
private var smallLayout: some View {
|
||||
VStack(spacing: 8) {
|
||||
// Top row: Great, Good, Average
|
||||
VStack(spacing: 0) {
|
||||
// Top half: Great, Good, Average
|
||||
HStack(spacing: 12) {
|
||||
ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
|
||||
moodButton(for: mood, size: 40)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
|
||||
// Bottom row: Bad, Horrible
|
||||
// Bottom half: Bad, Horrible
|
||||
HStack(spacing: 12) {
|
||||
ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButton(for: mood, size: 40)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
// MARK: - Medium Widget: Single row
|
||||
// MARK: - Medium Widget: Vertical split - text top, voting bottom
|
||||
private var mediumLayout: some View {
|
||||
VStack(spacing: 12) {
|
||||
VStack(spacing: 0) {
|
||||
// Top: Text left-aligned, centered horizontally
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
// Bottom: Voting buttons with equal spacing, centered
|
||||
HStack(spacing: 0) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButtonMedium(for: mood)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -146,29 +149,40 @@ struct LargeVotingView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 0) {
|
||||
// Top 25%: Title centered x,y
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
.padding(.horizontal, 12)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.frame(height: geo.size.height * 0.25)
|
||||
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
.padding(.horizontal, 8)
|
||||
// Bottom 75%: Voting buttons in two rows
|
||||
VStack(spacing: 0) {
|
||||
// Top row at 33%: Great, Good, Average
|
||||
HStack(spacing: 16) {
|
||||
ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
|
||||
moodButton(for: mood)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// Large mood buttons in a row - flexible spacing
|
||||
HStack(spacing: 0) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButton(for: mood)
|
||||
.frame(maxWidth: .infinity)
|
||||
// Bottom row at 66%: Bad, Horrible
|
||||
HStack(spacing: 16) {
|
||||
ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButton(for: mood)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.frame(height: geo.size.height * 0.75)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@@ -7,7 +7,5 @@
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.tt.feelsDebug</string>
|
||||
<string>iCloud.com.88oakapps.feels</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
@@ -12,7 +14,7 @@
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.feelsDebug</string>
|
||||
<string>group.com.88oakapps.feels</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.tt.feelsDebug</string>
|
||||
<string>iCloud.com.88oakapps.feels.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
@@ -14,7 +14,7 @@
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.feelsDebug</string>
|
||||
<string>group.com.88oakapps.feels.debug</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -137,7 +137,7 @@ Three widget types in `FeelsWidget/`:
|
||||
2. **FeelsGraphicWidget**: Small widget with mood graphic
|
||||
3. **FeelsIconWidget**: Custom icon widget
|
||||
|
||||
Data shared via App Groups: `group.com.88oakapps.ifeel`
|
||||
Data shared via App Groups: `group.com.88oakapps.feels`
|
||||
|
||||
---
|
||||
|
||||
@@ -155,8 +155,8 @@ Data shared via App Groups: `group.com.88oakapps.ifeel`
|
||||
## Configuration
|
||||
|
||||
### App Groups
|
||||
- Production: `group.com.88oakapps.ifeel`
|
||||
- Debug: `group.com.88oakapps.ifeelDebug`
|
||||
- Production: `group.com.88oakapps.feels`
|
||||
- Debug: `group.com.88oakapps.feels.debug`
|
||||
|
||||
### Background Tasks
|
||||
- Identifier: `com.88oak.Feels.dbUpdateMissing`
|
||||
|
||||
@@ -241,29 +241,40 @@ struct FeelsTipModifier: ViewModifier {
|
||||
let tip: any FeelsTip
|
||||
let gradientColors: [Color]
|
||||
|
||||
@ObservedObject private var tipsManager = FeelsTipsManager.shared
|
||||
// Use local state for sheet to avoid interference from other manager state changes
|
||||
@State private var showSheet = false
|
||||
@State private var hasCheckedEligibility = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear {
|
||||
tipsManager.showTipIfEligible(tip)
|
||||
}
|
||||
.sheet(isPresented: $tipsManager.showTipModal) {
|
||||
if let currentTip = tipsManager.currentTip {
|
||||
TipModalView(
|
||||
icon: currentTip.icon,
|
||||
title: currentTip.title,
|
||||
message: currentTip.message,
|
||||
gradientColors: gradientColors,
|
||||
onDismiss: {
|
||||
tipsManager.markTipAsShown(currentTip)
|
||||
}
|
||||
)
|
||||
.presentationDetents([.height(340)])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationCornerRadius(28)
|
||||
// Only check eligibility once per view lifetime
|
||||
guard !hasCheckedEligibility else { return }
|
||||
hasCheckedEligibility = true
|
||||
|
||||
// Delay tip presentation to ensure view hierarchy is fully established
|
||||
// This prevents "presenting from detached view controller" errors
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
if FeelsTipsManager.shared.shouldShowTip(tip) {
|
||||
showSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSheet) {
|
||||
TipModalView(
|
||||
icon: tip.icon,
|
||||
title: tip.title,
|
||||
message: tip.message,
|
||||
gradientColors: gradientColors,
|
||||
onDismiss: {
|
||||
showSheet = false
|
||||
FeelsTipsManager.shared.markTipAsShown(tip)
|
||||
}
|
||||
)
|
||||
.presentationDetents([.height(340)])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationCornerRadius(28)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -261,6 +261,60 @@ class HealthKitManager: ObservableObject {
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Delete All Moods from HealthKit
|
||||
|
||||
/// Deletes all State of Mind samples created by this app
|
||||
/// Note: HealthKit only allows deleting samples that your app created
|
||||
func deleteAllMoods() async throws -> Int {
|
||||
guard isHealthKitAvailable else {
|
||||
throw HealthKitError.notAvailable
|
||||
}
|
||||
|
||||
guard let stateOfMindType = stateOfMindType else {
|
||||
throw HealthKitError.typeNotAvailable
|
||||
}
|
||||
|
||||
guard checkAuthorizationStatus() == .sharingAuthorized else {
|
||||
throw HealthKitError.notAuthorized
|
||||
}
|
||||
|
||||
logger.info("Starting deletion of all State of Mind samples from this app")
|
||||
|
||||
// Fetch all State of Mind samples (HealthKit will only return ones we can delete - our own)
|
||||
let samples = try await fetchMoods(
|
||||
from: Date(timeIntervalSince1970: 0),
|
||||
to: Date().addingTimeInterval(86400) // Include today + 1 day buffer
|
||||
)
|
||||
|
||||
guard !samples.isEmpty else {
|
||||
logger.info("No State of Mind samples found to delete")
|
||||
return 0
|
||||
}
|
||||
|
||||
logger.info("Found \(samples.count) State of Mind samples to delete")
|
||||
|
||||
// Delete in batches
|
||||
let batchSize = 50
|
||||
var deletedCount = 0
|
||||
|
||||
for batchStart in stride(from: 0, to: samples.count, by: batchSize) {
|
||||
let batchEnd = min(batchStart + batchSize, samples.count)
|
||||
let batch = Array(samples[batchStart..<batchEnd])
|
||||
|
||||
do {
|
||||
try await healthStore.delete(batch)
|
||||
deletedCount += batch.count
|
||||
logger.info("Deleted batch \(batchStart/batchSize + 1): \(batch.count) samples")
|
||||
} catch {
|
||||
logger.error("Failed to delete batch starting at \(batchStart): \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Successfully deleted \(deletedCount) State of Mind samples from HealthKit")
|
||||
return deletedCount
|
||||
}
|
||||
|
||||
// MARK: - Read Mood from HealthKit
|
||||
|
||||
func fetchMoods(from startDate: Date, to endDate: Date) async throws -> [HKStateOfMind] {
|
||||
|
||||
@@ -65,7 +65,7 @@ enum AppTheme: Int, CaseIterable, Identifiable {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .zenGarden:
|
||||
return "Japanese minimalism meets mindful awareness. Soft pastels, organic growth icons, brush-stroke entries, and contemplative vertical voting."
|
||||
return "Japanese minimalism meets mindful awareness. Soft pastels, organic growth icons, clean entries, and contemplative vertical voting."
|
||||
case .synthwave:
|
||||
return "80s arcade aesthetic with neon glow. Electric colors, cosmic icons, grid backgrounds, and equalizer-bar voting."
|
||||
case .celestial:
|
||||
@@ -75,7 +75,7 @@ enum AppTheme: Int, CaseIterable, Identifiable {
|
||||
case .mixtape:
|
||||
return "Cassette culture and analog warmth. Tape reels, track numbers, and the tactile feel of pressing play."
|
||||
case .bloom:
|
||||
return "From wilted flower to full bloom. Organic shapes, glowing orbs, and the gentle metaphor of growth."
|
||||
return "From wilted flower to full bloom. Atmospheric glowing entries, organic icons, and the gentle metaphor of growth."
|
||||
case .heartfelt:
|
||||
return "Unashamed emotional expression. Heart icons from broken to sparkling, bold colors, intuitive selection."
|
||||
case .minimal:
|
||||
@@ -83,7 +83,7 @@ enum AppTheme: Int, CaseIterable, Identifiable {
|
||||
case .luxe:
|
||||
return "Liquid glass and premium materials. Cutting-edge iOS design language for the discerning user."
|
||||
case .forecast:
|
||||
return "Your mood is the weather. Storm to sunshine icons, flowing wave entries, and natural intuition."
|
||||
return "Your mood is the weather. Storm to sunshine icons, colorful bubble entries, and natural intuition."
|
||||
case .playful:
|
||||
return "Life's too short to be serious. Vibrant neons, familiar emoji, and game-like interaction."
|
||||
case .journal:
|
||||
@@ -146,16 +146,16 @@ enum AppTheme: Int, CaseIterable, Identifiable {
|
||||
|
||||
var entryStyle: DayViewStyle {
|
||||
switch self {
|
||||
case .zenGarden: return .ink
|
||||
case .zenGarden: return .minimal
|
||||
case .synthwave: return .neon
|
||||
case .celestial: return .orbit
|
||||
case .editorial: return .chronicle
|
||||
case .mixtape: return .tape
|
||||
case .bloom: return .morph
|
||||
case .bloom: return .aura
|
||||
case .heartfelt: return .bubble
|
||||
case .minimal: return .minimal
|
||||
case .luxe: return .glass
|
||||
case .forecast: return .wave
|
||||
case .forecast: return .bubble
|
||||
case .playful: return .pattern
|
||||
case .journal: return .stack
|
||||
}
|
||||
@@ -169,10 +169,10 @@ enum AppTheme: Int, CaseIterable, Identifiable {
|
||||
case .editorial: return .horizontal
|
||||
case .mixtape: return .cards
|
||||
case .bloom: return .aura
|
||||
case .heartfelt: return .radial
|
||||
case .heartfelt: return .horizontal
|
||||
case .minimal: return .horizontal
|
||||
case .luxe: return .aura
|
||||
case .forecast: return .radial
|
||||
case .forecast: return .horizontal
|
||||
case .playful: return .cards
|
||||
case .journal: return .stacked
|
||||
}
|
||||
|
||||
@@ -9,23 +9,20 @@ import SwiftUI
|
||||
|
||||
class DaysFilterClass: ObservableObject {
|
||||
static let shared = DaysFilterClass()
|
||||
|
||||
@Published public var currentFilters = [Int]()
|
||||
|
||||
|
||||
// Always show all days (1-7 = Sunday through Saturday)
|
||||
@Published public var currentFilters = [1, 2, 3, 4, 5, 6, 7]
|
||||
|
||||
init() {
|
||||
let storedDays = UserDefaultsStore.getDaysFilter()
|
||||
currentFilters = storedDays
|
||||
// Always include all days
|
||||
currentFilters = [1, 2, 3, 4, 5, 6, 7]
|
||||
}
|
||||
|
||||
|
||||
func addFilter(newFilter: Int) {
|
||||
currentFilters.append(newFilter)
|
||||
currentFilters = UserDefaultsStore.saveDaysFilter(days: currentFilters)
|
||||
// No-op: always show all days
|
||||
}
|
||||
|
||||
|
||||
func removeFilter(filter: Int) {
|
||||
if let index = currentFilters.firstIndex(of: filter) {
|
||||
currentFilters.remove(at: index)
|
||||
}
|
||||
currentFilters = UserDefaultsStore.saveDaysFilter(days: currentFilters)
|
||||
// No-op: always show all days
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,15 @@ import Foundation
|
||||
enum VotingLayoutStyle: Int, CaseIterable {
|
||||
case horizontal = 0 // Current: 5 buttons in a row
|
||||
case cards = 1 // Larger tappable cards with labels
|
||||
case radial = 2 // Semi-circle/wheel arrangement
|
||||
case stacked = 3 // Full-width vertical list
|
||||
case aura = 4 // Atmospheric glowing orbs with flowing layout
|
||||
case orbit = 5 // Celestial orbit with center core
|
||||
case neon = 6 // Synthwave arcade equalizer with glowing segments
|
||||
case stacked = 2 // Full-width vertical list
|
||||
case aura = 3 // Atmospheric glowing orbs with flowing layout
|
||||
case orbit = 4 // Celestial orbit with center core
|
||||
case neon = 5 // Synthwave arcade equalizer with glowing segments
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .horizontal: return "Horizontal"
|
||||
case .cards: return "Cards"
|
||||
case .radial: return "Radial"
|
||||
case .stacked: return "Stacked"
|
||||
case .aura: return "Aura"
|
||||
case .orbit: return "Orbit"
|
||||
@@ -162,6 +160,21 @@ enum DayViewStyle: Int, CaseIterable {
|
||||
var isGridLayout: Bool {
|
||||
self == .grid
|
||||
}
|
||||
|
||||
/// Styles available in the picker (some are disabled/experimental)
|
||||
var isAvailable: Bool {
|
||||
switch self {
|
||||
case .motion, .leather, .wave, .morph, .prism, .ink:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// All styles available to users
|
||||
static var availableCases: [DayViewStyle] {
|
||||
allCases.filter { $0.isAvailable }
|
||||
}
|
||||
}
|
||||
|
||||
class UserDefaultsStore {
|
||||
|
||||
@@ -45,20 +45,6 @@ struct OnboardingCustomizeOne: View {
|
||||
.foregroundColor(.black)
|
||||
.multilineTextAlignment(.leading)
|
||||
IconPickerView()
|
||||
|
||||
Text(String(localized: "onboarding_title_customize_one_section_two_title"))
|
||||
.font(.title3)
|
||||
.padding()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.black)
|
||||
|
||||
DayFilterPickerView()
|
||||
|
||||
Text(String(localized: "onboarding_title_customize_one_section_two_note"))
|
||||
.font(.title3)
|
||||
.padding()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -47,9 +47,9 @@ final class ExtensionDataProvider {
|
||||
// Watch uses CloudKit for automatic sync with iPhone
|
||||
let cloudKitContainerID: String
|
||||
#if DEBUG
|
||||
cloudKitContainerID = "iCloud.com.tt.feelsDebug"
|
||||
cloudKitContainerID = "iCloud.com.88oakapps.feels.debug"
|
||||
#else
|
||||
cloudKitContainerID = "iCloud.com.tt.feels"
|
||||
cloudKitContainerID = "iCloud.com.88oakapps.feels"
|
||||
#endif
|
||||
|
||||
let configuration = ModelConfiguration(
|
||||
|
||||
@@ -107,9 +107,9 @@ enum SharedModelContainer {
|
||||
/// CloudKit container identifier based on build configuration
|
||||
static var cloudKitContainerID: String {
|
||||
#if DEBUG
|
||||
return "iCloud.com.tt.feelsDebug"
|
||||
return "iCloud.com.88oakapps.feels.debug"
|
||||
#else
|
||||
return "iCloud.com.tt.feels"
|
||||
return "iCloud.com.88oakapps.feels"
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct Constants {
|
||||
static let groupShareId = "group.com.tt.feels"
|
||||
static let groupShareIdDebug = "group.com.tt.feelsDebug"
|
||||
static let groupShareId = "group.com.88oakapps.feels"
|
||||
static let groupShareIdDebug = "group.com.88oakapps.feels.debug"
|
||||
|
||||
static var currentGroupShareId: String {
|
||||
#if DEBUG
|
||||
|
||||
@@ -77,8 +77,6 @@ struct AddMoodHeaderView: View {
|
||||
HorizontalVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .cards:
|
||||
CardVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .radial:
|
||||
RadialVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .stacked:
|
||||
StackedVotingView(moodTint: moodTint, onMoodSelected: addItem)
|
||||
case .aura:
|
||||
@@ -137,99 +135,62 @@ struct CardVotingView: View {
|
||||
let moodTint: MoodTints
|
||||
let onMoodSelected: (Mood) -> Void
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible(), spacing: 12),
|
||||
GridItem(.flexible(), spacing: 12),
|
||||
GridItem(.flexible(), spacing: 12)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: 12) {
|
||||
ForEach(Mood.allValues) { mood in
|
||||
Button(action: { onMoodSelected(mood) }) {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(moodTint.color(forMood: mood).opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Select this mood"))
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel(String(localized: "Mood selection"))
|
||||
}
|
||||
}
|
||||
GeometryReader { geo in
|
||||
let spacing: CGFloat = 12
|
||||
let cardWidth = (geo.size.width - spacing * 2) / 3
|
||||
// Offset to center bottom row cards between top row cards
|
||||
// Each bottom card should be centered between two top cards
|
||||
let bottomOffset = (cardWidth + spacing) / 2
|
||||
|
||||
// MARK: - Layout 3: Radial/Semi-circle
|
||||
struct RadialVotingView: View {
|
||||
let moodTint: MoodTints
|
||||
let onMoodSelected: (Mood) -> Void
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height * 0.9)
|
||||
let radius = min(geometry.size.width, geometry.size.height) * 0.65
|
||||
let moods = Mood.allValues
|
||||
|
||||
ZStack {
|
||||
ForEach(Array(moods.enumerated()), id: \.element.id) { index, mood in
|
||||
let angle = angleForIndex(index, total: moods.count)
|
||||
let position = positionForAngle(angle, radius: radius, center: center)
|
||||
|
||||
Button(action: { onMoodSelected(mood) }) {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.padding(12)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.1))
|
||||
)
|
||||
VStack(spacing: spacing) {
|
||||
// Top row: Great, Good, Average
|
||||
HStack(spacing: spacing) {
|
||||
ForEach(Array(Mood.allValues.prefix(3))) { mood in
|
||||
cardButton(for: mood, width: cardWidth)
|
||||
}
|
||||
.buttonStyle(MoodButtonStyle())
|
||||
.position(position)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Select this mood"))
|
||||
}
|
||||
|
||||
// Bottom row: Bad, Horrible - centered between top row items
|
||||
HStack(spacing: spacing) {
|
||||
ForEach(Array(Mood.allValues.suffix(2))) { mood in
|
||||
cardButton(for: mood, width: cardWidth)
|
||||
}
|
||||
}
|
||||
.padding(.leading, bottomOffset)
|
||||
.padding(.trailing, bottomOffset)
|
||||
}
|
||||
}
|
||||
.frame(height: 180)
|
||||
.frame(height: 190)
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel(String(localized: "Mood selection"))
|
||||
}
|
||||
|
||||
private func angleForIndex(_ index: Int, total: Int) -> Double {
|
||||
// Spread moods across a semi-circle (180 degrees), from left to right
|
||||
let startAngle = Double.pi // 180 degrees (left)
|
||||
let endAngle = 0.0 // 0 degrees (right)
|
||||
let step = (startAngle - endAngle) / Double(total - 1)
|
||||
return startAngle - (step * Double(index))
|
||||
}
|
||||
|
||||
private func positionForAngle(_ angle: Double, radius: CGFloat, center: CGPoint) -> CGPoint {
|
||||
CGPoint(
|
||||
x: center.x + radius * CGFloat(cos(angle)),
|
||||
y: center.y - radius * CGFloat(sin(angle))
|
||||
)
|
||||
private func cardButton(for mood: Mood, width: CGFloat) -> some View {
|
||||
Button(action: { onMoodSelected(mood) }) {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.frame(width: width)
|
||||
.padding(.vertical, 20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(moodTint.color(forMood: mood).opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Select this mood"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout 4: Stacked Full-width
|
||||
// MARK: - Layout 3: Stacked Full-width
|
||||
struct StackedVotingView: View {
|
||||
let moodTint: MoodTints
|
||||
let onMoodSelected: (Mood) -> Void
|
||||
|
||||
@@ -101,11 +101,6 @@ struct CustomizeContentView: View {
|
||||
SettingsSection(title: "Notifications") {
|
||||
PersonalityPackPickerCompact()
|
||||
}
|
||||
|
||||
// FILTERS
|
||||
SettingsSection(title: "Day Filter") {
|
||||
DayFilterPickerCompact()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 32)
|
||||
@@ -175,11 +170,6 @@ struct CustomizeView: View {
|
||||
SettingsSection(title: "Notifications") {
|
||||
PersonalityPackPickerCompact()
|
||||
}
|
||||
|
||||
// FILTERS
|
||||
SettingsSection(title: "Day Filter") {
|
||||
DayFilterPickerCompact()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 32)
|
||||
@@ -406,14 +396,6 @@ struct VotingLayoutPickerCompact: View {
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) {
|
||||
ForEach(0..<6, id: \.self) { _ in RoundedRectangle(cornerRadius: 3).frame(width: 10, height: 12) }
|
||||
}
|
||||
case .radial:
|
||||
ZStack {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
Circle()
|
||||
.frame(width: 7, height: 7)
|
||||
.offset(radialOffset(index: index, total: 5, radius: 15))
|
||||
}
|
||||
}
|
||||
case .stacked:
|
||||
VStack(spacing: 4) {
|
||||
ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 2).frame(width: 32, height: 7) }
|
||||
@@ -486,11 +468,6 @@ struct VotingLayoutPickerCompact: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func radialOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
|
||||
let angle = Double.pi - (Double.pi * Double(index) / Double(total - 1))
|
||||
return CGSize(width: radius * CGFloat(cos(angle)), height: -radius * CGFloat(sin(angle)) + 4)
|
||||
}
|
||||
|
||||
private func orbitOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
|
||||
let startAngle = -Double.pi / 2
|
||||
let angleStep = (2 * Double.pi) / Double(total)
|
||||
@@ -627,59 +604,6 @@ struct PersonalityPackPickerCompact: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Day Filter Picker
|
||||
struct DayFilterPickerCompact: View {
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let weekdays = [(Calendar.current.shortWeekdaySymbols[0], 1),
|
||||
(Calendar.current.shortWeekdaySymbols[1], 2),
|
||||
(Calendar.current.shortWeekdaySymbols[2], 3),
|
||||
(Calendar.current.shortWeekdaySymbols[3], 4),
|
||||
(Calendar.current.shortWeekdaySymbols[4], 5),
|
||||
(Calendar.current.shortWeekdaySymbols[5], 6),
|
||||
(Calendar.current.shortWeekdaySymbols[6], 7)]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 14) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(weekdays.indices, id: \.self) { dayIdx in
|
||||
let day = String(weekdays[dayIdx].0)
|
||||
let value = weekdays[dayIdx].1
|
||||
let isActive = filteredDays.currentFilters.contains(value)
|
||||
|
||||
Button(action: {
|
||||
if isActive {
|
||||
filteredDays.removeFilter(filter: value)
|
||||
} else {
|
||||
filteredDays.addFilter(newFilter: value)
|
||||
}
|
||||
let impactMed = UIImpactFeedbackGenerator(style: .medium)
|
||||
impactMed.impactOccurred()
|
||||
}) {
|
||||
Text(day.prefix(2).uppercased())
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(isActive ? .white : theme.currentTheme.labelColor.opacity(0.5))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 40)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(isActive ? Color.accentColor : (colorScheme == .dark ? Color(.systemGray5) : .white))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
Text(String(localized: "day_picker_view_text"))
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.currentTheme.labelColor.opacity(0.5))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscription Banner
|
||||
struct SubscriptionBannerView: View {
|
||||
@Binding var showSubscriptionStore: Bool
|
||||
@@ -787,7 +711,7 @@ struct DayViewStylePickerCompact: View {
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in
|
||||
ForEach(DayViewStyle.availableCases, id: \.rawValue) { style in
|
||||
Button(action: {
|
||||
if UIAccessibility.isReduceMotionEnabled {
|
||||
dayViewStyle = style
|
||||
|
||||
@@ -90,14 +90,6 @@ struct VotingLayoutPickerView: View {
|
||||
.frame(width: 10, height: 12)
|
||||
}
|
||||
}
|
||||
case .radial:
|
||||
ZStack {
|
||||
ForEach(0..<5) { index in
|
||||
Circle()
|
||||
.frame(width: 6, height: 6)
|
||||
.offset(radialOffset(index: index, total: 5, radius: 16))
|
||||
}
|
||||
}
|
||||
case .stacked:
|
||||
VStack(spacing: 3) {
|
||||
ForEach(0..<4) { _ in
|
||||
@@ -179,14 +171,6 @@ struct VotingLayoutPickerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func radialOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
|
||||
let angle = Double.pi - (Double.pi * Double(index) / Double(total - 1))
|
||||
return CGSize(
|
||||
width: radius * CGFloat(cos(angle)),
|
||||
height: -radius * CGFloat(sin(angle)) + 4
|
||||
)
|
||||
}
|
||||
|
||||
private func orbitOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
|
||||
// Start from top (-π/2) and go clockwise
|
||||
let startAngle = -Double.pi / 2
|
||||
|
||||
@@ -21,6 +21,8 @@ struct SettingsContentView: View {
|
||||
@State private var showTrialDatePicker = false
|
||||
@State private var isExportingWidgets = false
|
||||
@State private var widgetExportPath: URL?
|
||||
@State private var isDeletingHealthKitData = false
|
||||
@State private var healthKitDeleteResult: String?
|
||||
@StateObject private var healthService = HealthService.shared
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
@@ -62,6 +64,7 @@ struct SettingsContentView: View {
|
||||
tipsPreviewButton
|
||||
testNotificationsButton
|
||||
exportWidgetsButton
|
||||
deleteHealthKitDataButton
|
||||
|
||||
clearDataButton
|
||||
#endif
|
||||
@@ -475,6 +478,58 @@ struct SettingsContentView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var deleteHealthKitDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button {
|
||||
isDeletingHealthKitData = true
|
||||
healthKitDeleteResult = nil
|
||||
Task {
|
||||
do {
|
||||
let count = try await HealthKitManager.shared.deleteAllMoods()
|
||||
healthKitDeleteResult = "✓ Deleted \(count) records"
|
||||
} catch {
|
||||
healthKitDeleteResult = "✗ Error: \(error.localizedDescription)"
|
||||
}
|
||||
isDeletingHealthKitData = false
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
if isDeletingHealthKitData {
|
||||
ProgressView()
|
||||
.frame(width: 32)
|
||||
} else {
|
||||
Image(systemName: "heart.slash.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.red)
|
||||
.frame(width: 32)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Delete HealthKit Data")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
if let result = healthKitDeleteResult {
|
||||
Text(result)
|
||||
.font(.caption)
|
||||
.foregroundColor(result.contains("✓") ? .green : .red)
|
||||
} else {
|
||||
Text("Remove all State of Mind records")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.disabled(isDeletingHealthKitData)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var clearDataButton: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
Reference in New Issue
Block a user