feat: rebuild iOS app from API audit + new podcast/media service
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>
This commit is contained in:
@@ -1,49 +1,74 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
@MainActor @Observable
|
||||
@Observable
|
||||
final class HomeViewModel {
|
||||
var todayEntries: [FoodEntry] = []
|
||||
var goal: DailyGoal = DailyGoal()
|
||||
var isLoading = true
|
||||
var errorMessage: String?
|
||||
|
||||
private let repo = FitnessRepository.shared
|
||||
|
||||
var totalCalories: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.calories }
|
||||
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 totalProtein: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.protein }
|
||||
var hasBackground: Bool {
|
||||
backgroundImage != nil
|
||||
}
|
||||
|
||||
var totalCarbs: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.carbs }
|
||||
}
|
||||
|
||||
var totalFat: Double {
|
||||
todayEntries.reduce(0) { $0 + $1.fat }
|
||||
}
|
||||
|
||||
var entryCount: Int {
|
||||
todayEntries.count
|
||||
}
|
||||
|
||||
func load() async {
|
||||
func loadTodayData() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
let today = Date().apiDateString
|
||||
|
||||
do {
|
||||
async let entriesTask = repo.entries(for: today, forceRefresh: true)
|
||||
async let goalsTask = repo.goals(for: today)
|
||||
|
||||
todayEntries = try await entriesTask
|
||||
goal = try await goalsTask
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user