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:
Trey t
2026-01-29 10:01:49 -06:00
parent b6290403b0
commit 810ac2d649
28 changed files with 12289 additions and 13332 deletions

View File

@@ -34,7 +34,10 @@
"Bash(unzip:*)", "Bash(unzip:*)",
"Bash(plutil:*)", "Bash(plutil:*)",
"Bash(done)", "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": [ "ask": [
"Bash(git commit:*)", "Bash(git commit:*)",

View File

@@ -30,8 +30,8 @@ Core Data operations are split across files in `Shared/Persistence/`:
## App Groups ## App Groups
- **Production**: `group.com.88oakapps.ifeel` - **Production**: `group.com.88oakapps.feels`
- **Debug**: `group.com.88oakapps.ifeelDebug` - **Debug**: `group.com.88oakapps.feels.debug`
## Build & Run ## Build & Run

View File

@@ -2,9 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <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> <key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>iCloud.com.tt.feelsDebug</string> <string>iCloud.com.88oakapps.feels</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
@@ -12,13 +20,7 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.tt.feelsDebug</string> <string>group.com.88oakapps.feels</string>
</array>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array>
<string>health-records</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -4,9 +4,13 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <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> <key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>iCloud.com.tt.feelsDebug</string> <string>iCloud.com.88oakapps.feels.debug</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
@@ -14,11 +18,7 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.tt.feelsDebug</string> <string>group.com.88oakapps.feels.debug</string>
</array> </array>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array/>
</dict> </dict>
</plist> </plist>

View File

@@ -2,17 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>group.com.tt.feels</string> <string>iCloud.com.88oakapps.feels</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
<string>CloudKit</string> <string>CloudKit</string>
</array> </array>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>iCloud.com.tt.feels</string> <string>group.com.88oakapps.feels</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -2,17 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>group.com.tt.feelsDebug</string> <string>iCloud.com.88oakapps.feels.debug</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
<string>CloudKit</string> <string>CloudKit</string>
</array> </array>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>iCloud.com.tt.feelsDebug</string> <string>group.com.88oakapps.feels.debug</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -23,17 +23,5 @@
<string>processing</string> <string>processing</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </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> </dict>
</plist> </plist>

View File

@@ -578,6 +578,7 @@
CODE_SIGN_ENTITLEMENTS = "Feels Watch App/Feels Watch AppDebug.entitlements"; CODE_SIGN_ENTITLEMENTS = "Feels Watch App/Feels Watch AppDebug.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = QND55P4443;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Feels; INFOPLIST_KEY_CFBundleDisplayName = Feels;
@@ -589,7 +590,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.watch.debug;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -733,10 +734,17 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = QND55P4443;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Feels--iOS--Info.plist"; INFOPLIST_FILE = "Feels--iOS--Info.plist";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -748,7 +756,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug; PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.debug;
PRODUCT_NAME = Feels; PRODUCT_NAME = Feels;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -768,10 +776,17 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = QND55P4443;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Feels--iOS--Info.plist"; INFOPLIST_FILE = "Feels--iOS--Info.plist";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -783,7 +798,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug; PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels;
PRODUCT_NAME = Feels; PRODUCT_NAME = Feels;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -938,10 +953,12 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = QND55P4443;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "FeelsWidgetExtension-Info.plist"; INFOPLIST_FILE = "FeelsWidgetExtension-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = FeelsWidget; INFOPLIST_KEY_CFBundleDisplayName = FeelsWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.6; IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -949,7 +966,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug.FeelsWidgetDebug; PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.widget.debug;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -970,10 +987,12 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = QND55P4443;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "FeelsWidgetExtension-Info.plist"; INFOPLIST_FILE = "FeelsWidgetExtension-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = FeelsWidget; INFOPLIST_KEY_CFBundleDisplayName = FeelsWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.6; IPHONEOS_DEPLOYMENT_TARGET = 18.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -981,7 +1000,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feelsDebug.FeelsWidget; PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.widget;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1002,18 +1021,19 @@
CODE_SIGN_ENTITLEMENTS = "Feels Watch App/Feels Watch App.entitlements"; CODE_SIGN_ENTITLEMENTS = "Feels Watch App/Feels Watch App.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23; CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_TEAM = QND55P4443;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Feels; INFOPLIST_KEY_CFBundleDisplayName = Feels;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.tt.feels; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.tt.feelsDebug;
INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.2; MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.feels.watchkitapp; PRODUCT_BUNDLE_IDENTIFIER = com.88oakapps.feels.watch;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos; SDKROOT = watchos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@@ -12,12 +12,12 @@
<key>Feels (macOS).xcscheme_^#shared#^_</key> <key>Feels (macOS).xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>3</integer>
</dict> </dict>
<key>Feels Watch App.xcscheme_^#shared#^_</key> <key>Feels Watch App.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>2</integer>
</dict> </dict>
<key>FeelsWidgetExtension.xcscheme_^#shared#^_</key> <key>FeelsWidgetExtension.xcscheme_^#shared#^_</key>
<dict> <dict>

File diff suppressed because it is too large Load Diff

View File

@@ -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 { private var smallLayout: some View {
VStack(spacing: 8) { VStack(spacing: 0) {
// Top row: Great, Good, Average // Top half: Great, Good, Average
HStack(spacing: 12) { HStack(spacing: 12) {
ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
moodButton(for: mood, size: 40) moodButton(for: mood, size: 40)
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
// Bottom row: Bad, Horrible // Bottom half: Bad, Horrible
HStack(spacing: 12) { HStack(spacing: 12) {
ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
moodButton(for: mood, size: 40) 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 { 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") Text(hasSubscription ? promptText : "Subscribe to track your mood")
.font(.headline) .font(.headline)
.foregroundStyle(.primary) .foregroundStyle(.primary)
.multilineTextAlignment(.center) .multilineTextAlignment(.leading)
.lineLimit(2) .lineLimit(2)
.minimumScaleFactor(0.8) .minimumScaleFactor(0.8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.padding(.horizontal, 16)
// Bottom: Voting buttons with equal spacing, centered
HStack(spacing: 0) { HStack(spacing: 0) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
moodButtonMedium(for: mood) moodButtonMedium(for: mood)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity)
} }
.padding(.horizontal, 12)
.padding(.vertical, 16)
} }
@ViewBuilder @ViewBuilder
@@ -146,29 +149,40 @@ struct LargeVotingView: View {
} }
var body: some View { var body: some View {
VStack(spacing: 16) { GeometryReader { geo in
Spacer() 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") // Bottom 75%: Voting buttons in two rows
.font(.title3.weight(.semibold)) VStack(spacing: 0) {
.foregroundStyle(.primary) // Top row at 33%: Great, Good, Average
.multilineTextAlignment(.center) HStack(spacing: 16) {
.lineLimit(2) ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
.minimumScaleFactor(0.8) moodButton(for: mood)
.padding(.horizontal, 8) }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Large mood buttons in a row - flexible spacing // Bottom row at 66%: Bad, Horrible
HStack(spacing: 0) { HStack(spacing: 16) {
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
moodButton(for: mood) moodButton(for: mood)
.frame(maxWidth: .infinity) }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} }
.frame(height: geo.size.height * 0.75)
} }
Spacer()
} }
.padding(.horizontal, 12)
.padding(.vertical, 16)
} }
@ViewBuilder @ViewBuilder

View File

@@ -7,7 +7,5 @@
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string> <string>com.apple.widgetkit-extension</string>
</dict> </dict>
<key>NSSupportsLiveActivities</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -2,9 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>iCloud.com.tt.feelsDebug</string> <string>iCloud.com.88oakapps.feels</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
@@ -12,7 +14,7 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.tt.feelsDebug</string> <string>group.com.88oakapps.feels</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -6,7 +6,7 @@
<string>development</string> <string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key> <key>com.apple.developer.icloud-container-identifiers</key>
<array> <array>
<string>iCloud.com.tt.feelsDebug</string> <string>iCloud.com.88oakapps.feels.debug</string>
</array> </array>
<key>com.apple.developer.icloud-services</key> <key>com.apple.developer.icloud-services</key>
<array> <array>
@@ -14,7 +14,7 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.tt.feelsDebug</string> <string>group.com.88oakapps.feels.debug</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -137,7 +137,7 @@ Three widget types in `FeelsWidget/`:
2. **FeelsGraphicWidget**: Small widget with mood graphic 2. **FeelsGraphicWidget**: Small widget with mood graphic
3. **FeelsIconWidget**: Custom icon widget 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 ## Configuration
### App Groups ### App Groups
- Production: `group.com.88oakapps.ifeel` - Production: `group.com.88oakapps.feels`
- Debug: `group.com.88oakapps.ifeelDebug` - Debug: `group.com.88oakapps.feels.debug`
### Background Tasks ### Background Tasks
- Identifier: `com.88oak.Feels.dbUpdateMissing` - Identifier: `com.88oak.Feels.dbUpdateMissing`

View File

@@ -241,29 +241,40 @@ struct FeelsTipModifier: ViewModifier {
let tip: any FeelsTip let tip: any FeelsTip
let gradientColors: [Color] 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 { func body(content: Content) -> some View {
content content
.onAppear { .onAppear {
tipsManager.showTipIfEligible(tip) // Only check eligibility once per view lifetime
} guard !hasCheckedEligibility else { return }
.sheet(isPresented: $tipsManager.showTipModal) { hasCheckedEligibility = true
if let currentTip = tipsManager.currentTip {
TipModalView( // Delay tip presentation to ensure view hierarchy is fully established
icon: currentTip.icon, // This prevents "presenting from detached view controller" errors
title: currentTip.title, DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
message: currentTip.message, if FeelsTipsManager.shared.shouldShowTip(tip) {
gradientColors: gradientColors, showSheet = true
onDismiss: { }
tipsManager.markTipAsShown(currentTip)
}
)
.presentationDetents([.height(340)])
.presentationDragIndicator(.visible)
.presentationCornerRadius(28)
} }
} }
.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)
}
} }
} }

View File

@@ -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 // MARK: - Read Mood from HealthKit
func fetchMoods(from startDate: Date, to endDate: Date) async throws -> [HKStateOfMind] { func fetchMoods(from startDate: Date, to endDate: Date) async throws -> [HKStateOfMind] {

View File

@@ -65,7 +65,7 @@ enum AppTheme: Int, CaseIterable, Identifiable {
var description: String { var description: String {
switch self { switch self {
case .zenGarden: 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: case .synthwave:
return "80s arcade aesthetic with neon glow. Electric colors, cosmic icons, grid backgrounds, and equalizer-bar voting." return "80s arcade aesthetic with neon glow. Electric colors, cosmic icons, grid backgrounds, and equalizer-bar voting."
case .celestial: case .celestial:
@@ -75,7 +75,7 @@ enum AppTheme: Int, CaseIterable, Identifiable {
case .mixtape: case .mixtape:
return "Cassette culture and analog warmth. Tape reels, track numbers, and the tactile feel of pressing play." return "Cassette culture and analog warmth. Tape reels, track numbers, and the tactile feel of pressing play."
case .bloom: 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: case .heartfelt:
return "Unashamed emotional expression. Heart icons from broken to sparkling, bold colors, intuitive selection." return "Unashamed emotional expression. Heart icons from broken to sparkling, bold colors, intuitive selection."
case .minimal: case .minimal:
@@ -83,7 +83,7 @@ enum AppTheme: Int, CaseIterable, Identifiable {
case .luxe: case .luxe:
return "Liquid glass and premium materials. Cutting-edge iOS design language for the discerning user." return "Liquid glass and premium materials. Cutting-edge iOS design language for the discerning user."
case .forecast: 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: case .playful:
return "Life's too short to be serious. Vibrant neons, familiar emoji, and game-like interaction." return "Life's too short to be serious. Vibrant neons, familiar emoji, and game-like interaction."
case .journal: case .journal:
@@ -146,16 +146,16 @@ enum AppTheme: Int, CaseIterable, Identifiable {
var entryStyle: DayViewStyle { var entryStyle: DayViewStyle {
switch self { switch self {
case .zenGarden: return .ink case .zenGarden: return .minimal
case .synthwave: return .neon case .synthwave: return .neon
case .celestial: return .orbit case .celestial: return .orbit
case .editorial: return .chronicle case .editorial: return .chronicle
case .mixtape: return .tape case .mixtape: return .tape
case .bloom: return .morph case .bloom: return .aura
case .heartfelt: return .bubble case .heartfelt: return .bubble
case .minimal: return .minimal case .minimal: return .minimal
case .luxe: return .glass case .luxe: return .glass
case .forecast: return .wave case .forecast: return .bubble
case .playful: return .pattern case .playful: return .pattern
case .journal: return .stack case .journal: return .stack
} }
@@ -169,10 +169,10 @@ enum AppTheme: Int, CaseIterable, Identifiable {
case .editorial: return .horizontal case .editorial: return .horizontal
case .mixtape: return .cards case .mixtape: return .cards
case .bloom: return .aura case .bloom: return .aura
case .heartfelt: return .radial case .heartfelt: return .horizontal
case .minimal: return .horizontal case .minimal: return .horizontal
case .luxe: return .aura case .luxe: return .aura
case .forecast: return .radial case .forecast: return .horizontal
case .playful: return .cards case .playful: return .cards
case .journal: return .stacked case .journal: return .stacked
} }

View File

@@ -9,23 +9,20 @@ import SwiftUI
class DaysFilterClass: ObservableObject { class DaysFilterClass: ObservableObject {
static let shared = DaysFilterClass() 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() { init() {
let storedDays = UserDefaultsStore.getDaysFilter() // Always include all days
currentFilters = storedDays currentFilters = [1, 2, 3, 4, 5, 6, 7]
} }
func addFilter(newFilter: Int) { func addFilter(newFilter: Int) {
currentFilters.append(newFilter) // No-op: always show all days
currentFilters = UserDefaultsStore.saveDaysFilter(days: currentFilters)
} }
func removeFilter(filter: Int) { func removeFilter(filter: Int) {
if let index = currentFilters.firstIndex(of: filter) { // No-op: always show all days
currentFilters.remove(at: index)
}
currentFilters = UserDefaultsStore.saveDaysFilter(days: currentFilters)
} }
} }

View File

@@ -10,17 +10,15 @@ import Foundation
enum VotingLayoutStyle: Int, CaseIterable { enum VotingLayoutStyle: Int, CaseIterable {
case horizontal = 0 // Current: 5 buttons in a row case horizontal = 0 // Current: 5 buttons in a row
case cards = 1 // Larger tappable cards with labels case cards = 1 // Larger tappable cards with labels
case radial = 2 // Semi-circle/wheel arrangement case stacked = 2 // Full-width vertical list
case stacked = 3 // Full-width vertical list case aura = 3 // Atmospheric glowing orbs with flowing layout
case aura = 4 // Atmospheric glowing orbs with flowing layout case orbit = 4 // Celestial orbit with center core
case orbit = 5 // Celestial orbit with center core case neon = 5 // Synthwave arcade equalizer with glowing segments
case neon = 6 // Synthwave arcade equalizer with glowing segments
var displayName: String { var displayName: String {
switch self { switch self {
case .horizontal: return "Horizontal" case .horizontal: return "Horizontal"
case .cards: return "Cards" case .cards: return "Cards"
case .radial: return "Radial"
case .stacked: return "Stacked" case .stacked: return "Stacked"
case .aura: return "Aura" case .aura: return "Aura"
case .orbit: return "Orbit" case .orbit: return "Orbit"
@@ -162,6 +160,21 @@ enum DayViewStyle: Int, CaseIterable {
var isGridLayout: Bool { var isGridLayout: Bool {
self == .grid 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 { class UserDefaultsStore {

View File

@@ -45,20 +45,6 @@ struct OnboardingCustomizeOne: View {
.foregroundColor(.black) .foregroundColor(.black)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
IconPickerView() 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() .padding()

View File

@@ -47,9 +47,9 @@ final class ExtensionDataProvider {
// Watch uses CloudKit for automatic sync with iPhone // Watch uses CloudKit for automatic sync with iPhone
let cloudKitContainerID: String let cloudKitContainerID: String
#if DEBUG #if DEBUG
cloudKitContainerID = "iCloud.com.tt.feelsDebug" cloudKitContainerID = "iCloud.com.88oakapps.feels.debug"
#else #else
cloudKitContainerID = "iCloud.com.tt.feels" cloudKitContainerID = "iCloud.com.88oakapps.feels"
#endif #endif
let configuration = ModelConfiguration( let configuration = ModelConfiguration(

View File

@@ -107,9 +107,9 @@ enum SharedModelContainer {
/// CloudKit container identifier based on build configuration /// CloudKit container identifier based on build configuration
static var cloudKitContainerID: String { static var cloudKitContainerID: String {
#if DEBUG #if DEBUG
return "iCloud.com.tt.feelsDebug" return "iCloud.com.88oakapps.feels.debug"
#else #else
return "iCloud.com.tt.feels" return "iCloud.com.88oakapps.feels"
#endif #endif
} }

View File

@@ -10,8 +10,8 @@ import SwiftUI
import SwiftData import SwiftData
struct Constants { struct Constants {
static let groupShareId = "group.com.tt.feels" static let groupShareId = "group.com.88oakapps.feels"
static let groupShareIdDebug = "group.com.tt.feelsDebug" static let groupShareIdDebug = "group.com.88oakapps.feels.debug"
static var currentGroupShareId: String { static var currentGroupShareId: String {
#if DEBUG #if DEBUG

View File

@@ -77,8 +77,6 @@ struct AddMoodHeaderView: View {
HorizontalVotingView(moodTint: moodTint, onMoodSelected: addItem) HorizontalVotingView(moodTint: moodTint, onMoodSelected: addItem)
case .cards: case .cards:
CardVotingView(moodTint: moodTint, onMoodSelected: addItem) CardVotingView(moodTint: moodTint, onMoodSelected: addItem)
case .radial:
RadialVotingView(moodTint: moodTint, onMoodSelected: addItem)
case .stacked: case .stacked:
StackedVotingView(moodTint: moodTint, onMoodSelected: addItem) StackedVotingView(moodTint: moodTint, onMoodSelected: addItem)
case .aura: case .aura:
@@ -137,99 +135,62 @@ struct CardVotingView: View {
let moodTint: MoodTints let moodTint: MoodTints
let onMoodSelected: (Mood) -> Void let onMoodSelected: (Mood) -> Void
private let columns = [
GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12),
GridItem(.flexible(), spacing: 12)
]
var body: some View { var body: some View {
LazyVGrid(columns: columns, spacing: 12) { GeometryReader { geo in
ForEach(Mood.allValues) { mood in let spacing: CGFloat = 12
Button(action: { onMoodSelected(mood) }) { let cardWidth = (geo.size.width - spacing * 2) / 3
mood.icon // Offset to center bottom row cards between top row cards
.resizable() // Each bottom card should be centered between two top cards
.aspectRatio(contentMode: .fit) let bottomOffset = (cardWidth + spacing) / 2
.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"))
}
}
// MARK: - Layout 3: Radial/Semi-circle VStack(spacing: spacing) {
struct RadialVotingView: View { // Top row: Great, Good, Average
let moodTint: MoodTints HStack(spacing: spacing) {
let onMoodSelected: (Mood) -> Void ForEach(Array(Mood.allValues.prefix(3))) { mood in
cardButton(for: mood, width: cardWidth)
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))
)
} }
.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) .accessibilityElement(children: .contain)
.accessibilityLabel(String(localized: "Mood selection")) .accessibilityLabel(String(localized: "Mood selection"))
} }
private func angleForIndex(_ index: Int, total: Int) -> Double { private func cardButton(for mood: Mood, width: CGFloat) -> some View {
// Spread moods across a semi-circle (180 degrees), from left to right Button(action: { onMoodSelected(mood) }) {
let startAngle = Double.pi // 180 degrees (left) mood.icon
let endAngle = 0.0 // 0 degrees (right) .resizable()
let step = (startAngle - endAngle) / Double(total - 1) .aspectRatio(contentMode: .fit)
return startAngle - (step * Double(index)) .frame(width: 40, height: 40)
} .foregroundColor(moodTint.color(forMood: mood))
.frame(width: width)
private func positionForAngle(_ angle: Double, radius: CGFloat, center: CGPoint) -> CGPoint { .padding(.vertical, 20)
CGPoint( .background(
x: center.x + radius * CGFloat(cos(angle)), RoundedRectangle(cornerRadius: 12)
y: center.y - radius * CGFloat(sin(angle)) .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 { struct StackedVotingView: View {
let moodTint: MoodTints let moodTint: MoodTints
let onMoodSelected: (Mood) -> Void let onMoodSelected: (Mood) -> Void

View File

@@ -101,11 +101,6 @@ struct CustomizeContentView: View {
SettingsSection(title: "Notifications") { SettingsSection(title: "Notifications") {
PersonalityPackPickerCompact() PersonalityPackPickerCompact()
} }
// FILTERS
SettingsSection(title: "Day Filter") {
DayFilterPickerCompact()
}
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.bottom, 32) .padding(.bottom, 32)
@@ -175,11 +170,6 @@ struct CustomizeView: View {
SettingsSection(title: "Notifications") { SettingsSection(title: "Notifications") {
PersonalityPackPickerCompact() PersonalityPackPickerCompact()
} }
// FILTERS
SettingsSection(title: "Day Filter") {
DayFilterPickerCompact()
}
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.bottom, 32) .padding(.bottom, 32)
@@ -406,14 +396,6 @@ struct VotingLayoutPickerCompact: View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) {
ForEach(0..<6, id: \.self) { _ in RoundedRectangle(cornerRadius: 3).frame(width: 10, height: 12) } 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: case .stacked:
VStack(spacing: 4) { VStack(spacing: 4) {
ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 2).frame(width: 32, height: 7) } 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 { private func orbitOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
let startAngle = -Double.pi / 2 let startAngle = -Double.pi / 2
let angleStep = (2 * Double.pi) / Double(total) 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 // MARK: - Subscription Banner
struct SubscriptionBannerView: View { struct SubscriptionBannerView: View {
@Binding var showSubscriptionStore: Bool @Binding var showSubscriptionStore: Bool
@@ -787,7 +711,7 @@ struct DayViewStylePickerCompact: View {
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) { HStack(spacing: 10) {
ForEach(DayViewStyle.allCases, id: \.rawValue) { style in ForEach(DayViewStyle.availableCases, id: \.rawValue) { style in
Button(action: { Button(action: {
if UIAccessibility.isReduceMotionEnabled { if UIAccessibility.isReduceMotionEnabled {
dayViewStyle = style dayViewStyle = style

View File

@@ -90,14 +90,6 @@ struct VotingLayoutPickerView: View {
.frame(width: 10, height: 12) .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: case .stacked:
VStack(spacing: 3) { VStack(spacing: 3) {
ForEach(0..<4) { _ in 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 { private func orbitOffset(index: Int, total: Int, radius: CGFloat) -> CGSize {
// Start from top (-π/2) and go clockwise // Start from top (-π/2) and go clockwise
let startAngle = -Double.pi / 2 let startAngle = -Double.pi / 2

View File

@@ -21,6 +21,8 @@ struct SettingsContentView: View {
@State private var showTrialDatePicker = false @State private var showTrialDatePicker = false
@State private var isExportingWidgets = false @State private var isExportingWidgets = false
@State private var widgetExportPath: URL? @State private var widgetExportPath: URL?
@State private var isDeletingHealthKitData = false
@State private var healthKitDeleteResult: String?
@StateObject private var healthService = HealthService.shared @StateObject private var healthService = HealthService.shared
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) @AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults)
@@ -62,6 +64,7 @@ struct SettingsContentView: View {
tipsPreviewButton tipsPreviewButton
testNotificationsButton testNotificationsButton
exportWidgetsButton exportWidgetsButton
deleteHealthKitDataButton
clearDataButton clearDataButton
#endif #endif
@@ -475,6 +478,58 @@ struct SettingsContentView: View {
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) .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 { private var clearDataButton: some View {
ZStack { ZStack {
theme.currentTheme.secondaryBGColor theme.currentTheme.secondaryBGColor