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:
@@ -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