Refactor widgets into separate focused files
Split the two large widget files (~2000 lines combined) into 10 focused files: - WidgetBundle.swift: Main @main bundle registration - WidgetModels.swift: Shared data models (WatchTimelineView, SimpleEntry, etc.) - WidgetProviders.swift: Timeline providers and TimeLineCreator - WidgetSharedViews.swift: Shared voting views - FeelsTimelineWidget.swift: Timeline widget (small/medium/large) - FeelsVoteWidget.swift: Vote widget with stats views - FeelsIconWidget.swift: Custom icon widget - FeelsGraphicWidget.swift: Graphic mood widget - FeelsMoodControlWidget.swift: Control Center widget - FeelsLiveActivity.swift: Live Activity with proper previews Preserves real-time update architecture (VoteMoodIntent, WidgetCenter, WidgetDataProvider patterns). Adds proper Live Activity preview support with sample content states. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
289
FeelsWidget2/WidgetSharedViews.swift
Normal file
289
FeelsWidget2/WidgetSharedViews.swift
Normal file
@@ -0,0 +1,289 @@
|
||||
//
|
||||
// WidgetSharedViews.swift
|
||||
// FeelsWidget
|
||||
//
|
||||
// Shared voting views used across multiple widgets
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import AppIntents
|
||||
|
||||
// MARK: - Voting View
|
||||
|
||||
struct VotingView: View {
|
||||
let family: WidgetFamily
|
||||
let promptText: String
|
||||
let hasSubscription: Bool
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
private var moodImages: MoodImagable.Type {
|
||||
UserDefaultsStore.moodMoodImagable()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if family == .systemSmall {
|
||||
smallLayout
|
||||
} else {
|
||||
mediumLayout
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Small Widget: 3 over 2 grid
|
||||
private var smallLayout: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text(hasSubscription ? promptText : "Tap to open app")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
// Top row: Great, Good, Average
|
||||
HStack(spacing: 12) {
|
||||
ForEach([Mood.great, .good, .average], id: \.rawValue) { mood in
|
||||
moodButton(for: mood, size: 36)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
|
||||
// Bottom row: Bad, Horrible
|
||||
HStack(spacing: 12) {
|
||||
ForEach([Mood.bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButton(for: mood, size: 36)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
// MARK: - Medium Widget: Single row
|
||||
private var mediumLayout: some View {
|
||||
VStack(spacing: 12) {
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach([Mood.great, .good, .average, .bad, .horrible], id: \.rawValue) { mood in
|
||||
moodButtonMedium(for: mood)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func moodButton(for mood: Mood, size: CGFloat) -> some View {
|
||||
// Used for small widget
|
||||
let touchSize = max(size, 44)
|
||||
|
||||
if hasSubscription {
|
||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||
moodIcon(for: mood, size: size)
|
||||
.frame(minWidth: touchSize, minHeight: touchSize)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
moodIcon(for: mood, size: size)
|
||||
.frame(minWidth: touchSize, minHeight: touchSize)
|
||||
}
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func moodButtonMedium(for mood: Mood) -> some View {
|
||||
// Medium widget uses smaller icons with labels, flexible width
|
||||
let content = VStack(spacing: 4) {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.caption2)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
|
||||
if hasSubscription {
|
||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||
content
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
content
|
||||
}
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||
}
|
||||
}
|
||||
|
||||
private func moodIcon(for mood: Mood, size: CGFloat) -> some View {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Large Voting View
|
||||
|
||||
struct LargeVotingView: View {
|
||||
let promptText: String
|
||||
let hasSubscription: Bool
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
private var moodImages: MoodImagable.Type {
|
||||
UserDefaultsStore.moodMoodImagable()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
|
||||
Text(hasSubscription ? promptText : "Subscribe to track your mood")
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.8)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func moodButton(for mood: Mood) -> some View {
|
||||
if hasSubscription {
|
||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||
moodButtonContent(for: mood)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
moodButtonContent(for: mood)
|
||||
}
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||
}
|
||||
}
|
||||
|
||||
private func moodButtonContent(for mood: Mood) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
|
||||
Text(mood.widgetDisplayName)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 4)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(moodTint.color(forMood: mood).opacity(0.15))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Inline Voting View (compact mood buttons for timeline widget)
|
||||
|
||||
struct InlineVotingView: View {
|
||||
let promptText: String
|
||||
let hasSubscription: Bool
|
||||
let moods: [Mood] = [.horrible, .bad, .average, .good, .great]
|
||||
|
||||
private var moodTint: MoodTintable.Type {
|
||||
UserDefaultsStore.moodTintable()
|
||||
}
|
||||
|
||||
private var moodImages: MoodImagable.Type {
|
||||
UserDefaultsStore.moodMoodImagable()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
Text(hasSubscription ? promptText : "Tap to open app")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.7)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ForEach(moods, id: \.rawValue) { mood in
|
||||
moodButton(for: mood)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func moodButton(for mood: Mood) -> some View {
|
||||
if hasSubscription {
|
||||
Button(intent: VoteMoodIntent(mood: mood)) {
|
||||
moodIcon(for: mood)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Log this mood"))
|
||||
} else {
|
||||
Link(destination: URL(string: "feels://subscribe")!) {
|
||||
moodIcon(for: mood)
|
||||
}
|
||||
.accessibilityLabel(mood.strValue)
|
||||
.accessibilityHint(String(localized: "Open app to subscribe"))
|
||||
}
|
||||
}
|
||||
|
||||
private func moodIcon(for mood: Mood) -> some View {
|
||||
moodImages.icon(forMood: mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user