Add interactive widget voting and fix warnings/bugs
Widget Features: - Add inline voting to timeline widget when no entry exists for today - Show random prompt from notification strings in voting mode - Update vote widget to use simple icon style for selection - Make stats bar full width in voted state view - Add Localizable.strings to widget extension target Bug Fixes: - Fix inverted date calculation in InsightsViewModel streak logic - Replace force unwraps with safe optional handling in widgets - Replace fatalError calls with graceful error handling - Fix CSV import safety in SettingsView Warning Fixes: - Add @retroactive to Color and Date extension conformances - Update deprecated onChange(of:perform:) to new syntax - Replace deprecated applicationIconBadgeNumber with setBadgeCount - Replace deprecated UIApplication.shared.windows API - Add @preconcurrency for Swift 6 protocol conformances - Add missing widget family cases to switch statement - Remove unused variables and #warning directives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,51 +176,51 @@ struct CreateWidgetView: View {
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_background_color"))
|
||||
ColorPicker("", selection: $customWidget.bgColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
.onChange(of: customWidget.bgColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_background_color")
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_inner_color"))
|
||||
ColorPicker("", selection: $customWidget.innerColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
.onChange(of: customWidget.innerColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_inner_color")
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_face_outline_color"))
|
||||
ColorPicker("", selection: $customWidget.circleStrokeColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
.onChange(of: customWidget.circleStrokeColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_outline_color")
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
}
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_left_eye_color"))
|
||||
ColorPicker("", selection: $customWidget.leftEyeColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
.onChange(of: customWidget.leftEyeColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_left_eye_color")
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_right_eye_color"))
|
||||
ColorPicker("", selection: $customWidget.rightEyeColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
.onChange(of: customWidget.rightEyeColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_right_eye_color")
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
@@ -228,9 +228,9 @@ struct CreateWidgetView: View {
|
||||
VStack(alignment: .center) {
|
||||
Text(String(localized: "create_widget_view_mouth_color"))
|
||||
ColorPicker("", selection: $customWidget.mouthColor)
|
||||
.onChange(of: customWidget.mouthColor, perform: { newValue in
|
||||
.onChange(of: customWidget.mouthColor) {
|
||||
EventLogger.log(event: "create_widget_view_update_mouth_color")
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
@@ -343,7 +343,7 @@ struct TintPickerCompact: View {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
ColorPicker("", selection: colorBinding(for: index))
|
||||
.labelsHidden()
|
||||
.onChange(of: colorBinding(for: index).wrappedValue) { _ in
|
||||
.onChange(of: colorBinding(for: index).wrappedValue) {
|
||||
saveCustomMoodTint()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,39 +60,39 @@ struct TintPickerView: View {
|
||||
|
||||
HStack {
|
||||
ColorPicker("", selection: $customMoodTint.colorOne)
|
||||
.onChange(of: customMoodTint.colorOne, perform: { _ in
|
||||
.onChange(of: customMoodTint.colorOne) {
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorTwo)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorTwo, perform: { _ in
|
||||
.onChange(of: customMoodTint.colorTwo) {
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorThree)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorThree, perform: { _ in
|
||||
.onChange(of: customMoodTint.colorThree) {
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorFour)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorFour, perform: { _ in
|
||||
.onChange(of: customMoodTint.colorFour) {
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
ColorPicker("", selection: $customMoodTint.colorFive)
|
||||
.labelsHidden()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.onChange(of: customMoodTint.colorFive, perform: { _ in
|
||||
.onChange(of: customMoodTint.colorFive) {
|
||||
saveCustomMoodTint()
|
||||
})
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Color.clear
|
||||
|
||||
@@ -65,7 +65,7 @@ class DayViewViewModel: ObservableObject {
|
||||
|
||||
public func update(entry: MoodEntryModel, toMood mood: Mood) {
|
||||
if !DataController.shared.update(entryDate: entry.forDate, withMood: mood) {
|
||||
#warning("show error")
|
||||
print("Failed to update mood entry")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@ struct HeaderStatsView : UIViewRepresentable {
|
||||
init(fakeData: Bool, backDays: Int, moodTint: [Color], textColor: Color) {
|
||||
self.moodTints = moodTint
|
||||
self.textColor = textColor
|
||||
guard moodTints.count == 5 else {
|
||||
fatalError("mood tint count dont match")
|
||||
}
|
||||
assert(moodTints.count == 5, "mood tint count should be 5")
|
||||
self.tmpHolderToMakeViewDiffefrent = Color.random()
|
||||
entries = [BarChartDataEntry]()
|
||||
|
||||
@@ -30,8 +28,10 @@ struct HeaderStatsView : UIViewRepresentable {
|
||||
if fakeData {
|
||||
moodEntries = DataController.shared.randomEntries(count: 10)
|
||||
} else {
|
||||
var daysAgo = Calendar.current.date(byAdding: .day, value: -backDays, to: Date())!
|
||||
daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgo)!
|
||||
guard let daysAgoDate = Calendar.current.date(byAdding: .day, value: -backDays, to: Date()),
|
||||
let daysAgo = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: daysAgoDate) else {
|
||||
return
|
||||
}
|
||||
|
||||
moodEntries = DataController.shared.getData(startDate: daysAgo, endDate: Date(), includedDays: [1,2,3,4,5,6,7])
|
||||
}
|
||||
|
||||
@@ -990,16 +990,20 @@ class InsightsViewModel: ObservableObject {
|
||||
var tempStreak = 1
|
||||
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let mostRecent = sortedEntries.first!.forDate
|
||||
if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
|
||||
guard let mostRecent = sortedEntries.first?.forDate,
|
||||
let yesterday = calendar.date(byAdding: .day, value: -1, to: today) else {
|
||||
return (0, 0)
|
||||
}
|
||||
|
||||
if calendar.isDate(mostRecent, inSameDayAs: today) || calendar.isDate(mostRecent, inSameDayAs: yesterday) {
|
||||
currentStreak = 1
|
||||
var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent)!
|
||||
var checkDate = calendar.date(byAdding: .day, value: -1, to: mostRecent) ?? mostRecent
|
||||
|
||||
for entry in sortedEntries.dropFirst() {
|
||||
let entryDate = entry.forDate
|
||||
if calendar.isDate(entryDate, inSameDayAs: checkDate) {
|
||||
currentStreak += 1
|
||||
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate)!
|
||||
checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -1009,7 +1013,7 @@ class InsightsViewModel: ObservableObject {
|
||||
for i in 1..<sortedEntries.count {
|
||||
let currentDate = sortedEntries[i].forDate
|
||||
let previousDate = sortedEntries[i-1].forDate
|
||||
let dayDiff = calendar.dateComponents([.day], from: currentDate, to: previousDate).day ?? 0
|
||||
let dayDiff = calendar.dateComponents([.day], from: previousDate, to: currentDate).day ?? 0
|
||||
if dayDiff == 1 {
|
||||
tempStreak += 1
|
||||
} else {
|
||||
|
||||
@@ -58,29 +58,27 @@ struct MainTabView: View {
|
||||
})
|
||||
})
|
||||
.onAppear(perform: {
|
||||
switch theme {
|
||||
case .system:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .iFeel:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .dark:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .light
|
||||
}
|
||||
})
|
||||
.onChange(of: theme, perform: { value in
|
||||
switch theme {
|
||||
case .system:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .iFeel:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .unspecified
|
||||
case .dark:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
|
||||
case .light:
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .light
|
||||
}
|
||||
applyTheme(theme)
|
||||
})
|
||||
.onChange(of: theme) { _, newTheme in
|
||||
applyTheme(newTheme)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyTheme(_ theme: Theme) {
|
||||
let style: UIUserInterfaceStyle
|
||||
switch theme {
|
||||
case .system, .iFeel:
|
||||
style = .unspecified
|
||||
case .dark:
|
||||
style = .dark
|
||||
case .light:
|
||||
style = .light
|
||||
}
|
||||
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first else { return }
|
||||
window.overrideUserInterfaceStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ struct SettingsView: View {
|
||||
defer { selectedFile.stopAccessingSecurityScopedResource() }
|
||||
|
||||
var rows = input.components(separatedBy: "\n")
|
||||
guard !rows.isEmpty else { return }
|
||||
rows.removeFirst()
|
||||
for row in rows {
|
||||
let stripped = row.replacingOccurrences(of: " +0000", with: "")
|
||||
@@ -119,10 +120,12 @@ struct SettingsView: View {
|
||||
if columns.count != 7 {
|
||||
continue
|
||||
}
|
||||
let forDate = dateFormatter.date(from: columns[3])!
|
||||
let moodValue = Int(columns[4])!
|
||||
guard let forDate = dateFormatter.date(from: columns[3]),
|
||||
let moodValue = Int(columns[4]) else {
|
||||
continue
|
||||
}
|
||||
let mood = Mood(rawValue: moodValue) ?? .missing
|
||||
let entryType = EntryType(rawValue: Int(columns[2])!) ?? .listView
|
||||
let entryType = EntryType(rawValue: Int(columns[2]) ?? 0) ?? .listView
|
||||
|
||||
DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType)
|
||||
}
|
||||
@@ -357,7 +360,7 @@ struct SettingsView: View {
|
||||
Text(String(localized: "settings_use_cloudkit_title"))
|
||||
.foregroundColor(textColor)
|
||||
})
|
||||
.onChange(of: useCloudKit) { newValue in
|
||||
.onChange(of: useCloudKit) { _, newValue in
|
||||
EventLogger.log(event: "toggle_use_cloudkit", withData: ["value": newValue])
|
||||
}
|
||||
.padding()
|
||||
@@ -391,7 +394,7 @@ struct SettingsView: View {
|
||||
VStack {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
isOn: $deleteEnabled)
|
||||
.onChange(of: deleteEnabled) { newValue in
|
||||
.onChange(of: deleteEnabled) { _, newValue in
|
||||
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
|
||||
@@ -49,7 +49,7 @@ struct AllMoodsTotalTemplate: View, SharingTemplate {
|
||||
$0.percent > $1.percent
|
||||
})
|
||||
} else {
|
||||
fatalError("no data")
|
||||
entries = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user