Add Apple platform features and UX improvements
- Add HealthKit State of Mind sync for mood entries - Add Live Activity with streak display and rating time window - Add App Shortcuts/Siri integration for voice mood logging - Add TipKit hints for feature discovery - Add centralized MoodLogger for consistent side effects - Add reminder time setting in Settings with time picker - Fix duplicate notifications when changing reminder time - Fix Live Activity streak showing 0 when not yet rated today - Fix slow tap response in entry detail mood selection - Update widget timeline to refresh at rating time - Sync widgets when reminder time changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,127 @@ import WidgetKit
|
||||
import SwiftUI
|
||||
import Intents
|
||||
import SwiftData
|
||||
import ActivityKit
|
||||
import AppIntents
|
||||
|
||||
// MARK: - Live Activity Widget
|
||||
// Note: MoodStreakAttributes is defined in MoodStreakActivity.swift (Shared folder)
|
||||
|
||||
struct MoodStreakLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: MoodStreakAttributes.self) { context in
|
||||
// Lock Screen / StandBy view
|
||||
MoodStreakLockScreenView(context: context)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
// Expanded view
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("\(context.state.currentStreak)")
|
||||
.font(.title2.bold())
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
if context.state.hasLoggedToday {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
.font(.title2)
|
||||
} else {
|
||||
Text("Log now")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
Text(context.state.hasLoggedToday ? "Streak: \(context.state.currentStreak) days" : "Don't break your streak!")
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
if !context.state.hasLoggedToday {
|
||||
Text("Voting closes at midnight")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color(hex: context.state.lastMoodColor))
|
||||
.frame(width: 20, height: 20)
|
||||
Text("Today: \(context.state.lastMoodLogged)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
} compactLeading: {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
} compactTrailing: {
|
||||
Text("\(context.state.currentStreak)")
|
||||
.font(.caption.bold())
|
||||
} minimal: {
|
||||
Image(systemName: "flame.fill")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MoodStreakLockScreenView: View {
|
||||
let context: ActivityViewContext<MoodStreakAttributes>
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
// Streak indicator
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: "flame.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.orange)
|
||||
Text("\(context.state.currentStreak)")
|
||||
.font(.title.bold())
|
||||
Text("day streak")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.frame(height: 50)
|
||||
|
||||
// Status
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if context.state.hasLoggedToday {
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(Color(hex: context.state.lastMoodColor))
|
||||
.frame(width: 24, height: 24)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Today's mood")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(context.state.lastMoodLogged)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Don't break your streak!")
|
||||
.font(.headline)
|
||||
Text("Tap to log your mood")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.activityBackgroundTint(Color(.systemBackground).opacity(0.8))
|
||||
}
|
||||
}
|
||||
|
||||
class WatchTimelineView: Identifiable {
|
||||
let id = UUID()
|
||||
@@ -355,12 +476,13 @@ struct LargeWidgetView: View {
|
||||
struct FeelsGraphicWidgetEntryView : View {
|
||||
@Environment(\.sizeCategory) var sizeCategory
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
|
||||
var entry: Provider.Entry
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
SmallGraphicWidgetView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,12 +517,13 @@ struct SmallGraphicWidgetView: View {
|
||||
struct FeelsIconWidgetEntryView : View {
|
||||
@Environment(\.sizeCategory) var sizeCategory
|
||||
@Environment(\.widgetFamily) var family
|
||||
|
||||
|
||||
var entry: Provider.Entry
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
SmallIconView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +649,31 @@ struct FeelsBundle: WidgetBundle {
|
||||
FeelsGraphicWidget()
|
||||
FeelsIconWidget()
|
||||
FeelsVoteWidget()
|
||||
FeelsMoodControlWidget()
|
||||
MoodStreakLiveActivity()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Control Center Widget
|
||||
struct FeelsMoodControlWidget: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(kind: "FeelsMoodControl") {
|
||||
ControlWidgetButton(action: OpenFeelsIntent()) {
|
||||
Label("Log Mood", systemImage: "face.smiling")
|
||||
}
|
||||
}
|
||||
.displayName("Log Mood")
|
||||
.description("Open Feels to log your mood")
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenFeelsIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Open Feels"
|
||||
static var description = IntentDescription("Open the Feels app to log your mood")
|
||||
static var openAppWhenRun: Bool = true
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user