iOS App (complete rebuild): - Audited all fitness API endpoints against live responses - Models match exact API field names (snapshot_ prefixes, UUID strings) - FoodEntry uses computed properties (foodName, calories, etc.) wrapping snapshot fields - Flexible Int/Double decoding for all numeric fields - AI assistant with raw JSON state management (JSONSerialization, not Codable) - Home dashboard with custom background, frosted glass calorie widget - Fitness: Today/Templates/Goals/Foods tabs - Food search with recent + all sections - Meal sections with colored accent bars, swipe to delete - 120fps ProMotion, iOS 17+ @Observable Podcast/Media Service: - FastAPI backend for podcast RSS + local audiobook folders - Shows, episodes, playback progress, queue management - RSS feed fetching with feedparser + ETag support - Local folder scanning with mutagen for audio metadata - HTTP Range streaming for local audio files - Playback events logging (play/pause/seek/complete) - Reuses brain's PostgreSQL + Redis - media_ prefixed tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.2 KiB
Swift
75 lines
2.2 KiB
Swift
import SwiftUI
|
|
import PhotosUI
|
|
|
|
@Observable
|
|
final class HomeViewModel {
|
|
private let repo = FitnessRepository.shared
|
|
|
|
var totalCalories: Double = 0
|
|
var calorieGoal: Double = 2000
|
|
var isLoading = false
|
|
|
|
// Background image
|
|
var backgroundImage: UIImage?
|
|
var selectedPhoto: PhotosPickerItem?
|
|
|
|
private let backgroundKey = "homeBackgroundImage"
|
|
|
|
init() {
|
|
loadSavedBackground()
|
|
}
|
|
|
|
var hasBackground: Bool {
|
|
backgroundImage != nil
|
|
}
|
|
|
|
func loadTodayData() async {
|
|
isLoading = true
|
|
let today = Date().apiDateString
|
|
async let entriesTask: () = repo.loadEntries(date: today)
|
|
async let goalTask: () = repo.loadGoal(date: today)
|
|
_ = await (entriesTask, goalTask)
|
|
totalCalories = repo.entries.reduce(0) { $0 + $1.snapshotCalories }
|
|
calorieGoal = repo.goal?.calories ?? 2000
|
|
isLoading = false
|
|
}
|
|
|
|
// MARK: - Background Image
|
|
|
|
func handlePhotoSelection() async {
|
|
guard let item = selectedPhoto else { return }
|
|
guard let data = try? await item.loadTransferable(type: Data.self) else { return }
|
|
guard let original = UIImage(data: data) else { return }
|
|
|
|
let resized = resizeImage(original, maxWidth: 1200)
|
|
backgroundImage = resized
|
|
|
|
if let jpegData = resized.jpegData(compressionQuality: 0.8) {
|
|
UserDefaults.standard.set(jpegData, forKey: backgroundKey)
|
|
}
|
|
selectedPhoto = nil
|
|
}
|
|
|
|
func removeBackground() {
|
|
backgroundImage = nil
|
|
UserDefaults.standard.removeObject(forKey: backgroundKey)
|
|
}
|
|
|
|
private func loadSavedBackground() {
|
|
if let data = UserDefaults.standard.data(forKey: backgroundKey),
|
|
let image = UIImage(data: data) {
|
|
backgroundImage = image
|
|
}
|
|
}
|
|
|
|
private func resizeImage(_ image: UIImage, maxWidth: CGFloat) -> UIImage {
|
|
let scale = maxWidth / image.size.width
|
|
guard scale < 1 else { return image }
|
|
let newSize = CGSize(width: maxWidth, height: image.size.height * scale)
|
|
let renderer = UIGraphicsImageRenderer(size: newSize)
|
|
return renderer.image { _ in
|
|
image.draw(in: CGRect(origin: .zero, size: newSize))
|
|
}
|
|
}
|
|
}
|