Add Progress Photos feature for plant growth tracking (Phase 8)

Implement progress photo capture with HEIC compression and thumbnail
generation, gallery view with grid display and full-size viewing,
time-lapse playback with adjustable speed, and photo reminder
notifications at weekly/biweekly/monthly intervals.

New files:
- ProgressPhoto domain entity with imageData and thumbnailData
- ProgressPhotoRepositoryProtocol and CoreDataProgressPhotoRepository
- CaptureProgressPhotoUseCase with image compression/resizing
- SchedulePhotoReminderUseCase with notification scheduling
- ProgressPhotosViewModel, ProgressPhotoGalleryView
- ProgressPhotoCaptureView, TimeLapsePlayerView

Modified:
- PlantMO with progressPhotos relationship
- Core Data model with ProgressPhotoMO entity
- NotificationService with photo reminder support
- PlantDetailView with Progress Photos section
- DIContainer with photo service registrations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-23 15:40:50 -06:00
parent f41c77876a
commit 4fcec31c02
17 changed files with 3315 additions and 12 deletions

View File

@@ -72,6 +72,9 @@ struct PlantDetailView: View {
// Identification info
identificationInfoSection
// Progress photos section
progressPhotosSection
}
}
.padding()
@@ -359,6 +362,64 @@ struct PlantDetailView: View {
.cornerRadius(12)
}
// MARK: - Progress Photos Section
private var progressPhotosSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Progress Photos")
.font(.headline)
Spacer()
Text("\(viewModel.progressPhotoCount) photos")
.font(.caption)
.foregroundStyle(.secondary)
}
NavigationLink {
ProgressPhotoGalleryView(
plantID: viewModel.plant.id,
plantName: viewModel.displayName
)
} label: {
HStack {
// Recent thumbnail or placeholder
if let recentThumbnail = viewModel.recentProgressPhotoThumbnail {
Image(uiImage: UIImage(data: recentThumbnail) ?? UIImage())
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
RoundedRectangle(cornerRadius: 8)
.fill(Color(.systemGray5))
.frame(width: 60, height: 60)
.overlay {
Image(systemName: "camera")
.foregroundStyle(.secondary)
}
}
VStack(alignment: .leading) {
Text("View Gallery")
.font(.subheadline)
Text("Track your plant's growth")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.secondary)
}
}
.buttonStyle(.plain)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
// MARK: - Private Helpers
private var identificationSourceDescription: String {