Files
voicediary/VoiceDiary/Views/DiaryEntryView.swift
Felix Förtsch dca03214b0 initial VoiceDiary iOS app setup
SwiftUI + SwiftData + iCloud, Apple Speech transcription (German),
audio recording, summarization service protocol (LLM-ready),
localization scaffolding (EN/DE/ES/FR), basic tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:57:41 +01:00

157 lines
3.8 KiB
Swift

import SwiftUI
struct DiaryEntryView: View {
@Environment(\.modelContext) private var modelContext
@Environment(DiaryViewModel.self) private var viewModel
let entry: DiaryEntry
@State private var selectedTab = 0
var body: some View {
VStack(spacing: 0) {
Picker(String(localized: "entry.viewMode"), selection: $selectedTab) {
Text(String(localized: "entry.tab.summary")).tag(0)
Text(String(localized: "entry.tab.transcripts")).tag(1)
Text(String(localized: "entry.tab.memos")).tag(2)
}
.pickerStyle(.segmented)
.padding()
TabView(selection: $selectedTab) {
summaryTab
.tag(0)
transcriptsTab
.tag(1)
memosTab
.tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.navigationTitle(entry.formattedDate)
.navigationBarTitleDisplayMode(.inline)
}
// MARK: - Summary Tab
private var summaryTab: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if let summary = entry.summary {
Text(LocalizedStringKey(summary))
.font(.body)
.textSelection(.enabled)
} else if viewModel.isSummarizing {
ProgressView(String(localized: "summary.generating"))
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 40)
} else if entry.hasTranscripts {
ContentUnavailableView {
Label(
String(localized: "summary.empty.title"),
systemImage: "text.document"
)
} description: {
Text(String(localized: "summary.empty.description"))
} actions: {
Button {
Task { await viewModel.generateSummary(for: entry) }
} label: {
Text(String(localized: "summary.generate"))
}
.buttonStyle(.borderedProminent)
}
} else {
ContentUnavailableView(
String(localized: "summary.noTranscripts.title"),
systemImage: "waveform.slash",
description: Text(String(localized: "summary.noTranscripts.description"))
)
}
}
.padding()
}
}
// MARK: - Transcripts Tab
private var transcriptsTab: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if entry.combinedTranscript.isEmpty {
ContentUnavailableView(
String(localized: "transcripts.empty.title"),
systemImage: "text.bubble",
description: Text(String(localized: "transcripts.empty.description"))
)
} else {
Text(entry.combinedTranscript)
.font(.body)
.textSelection(.enabled)
}
}
.padding()
}
}
// MARK: - Memos Tab
private var memosTab: some View {
List {
ForEach(entry.memos.sorted(by: { $0.recordedAt < $1.recordedAt })) { memo in
MemoRow(memo: memo)
}
.onDelete { indexSet in
let sorted = entry.memos.sorted { $0.recordedAt < $1.recordedAt }
for index in indexSet {
viewModel.deleteMemo(sorted[index], from: entry, context: modelContext)
}
}
}
.listStyle(.plain)
.overlay {
if entry.memos.isEmpty {
ContentUnavailableView(
String(localized: "memos.empty.title"),
systemImage: "mic.slash",
description: Text(String(localized: "memos.empty.description"))
)
}
}
}
}
struct MemoRow: View {
let memo: VoiceMemo
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Label(memo.formattedDuration, systemImage: "waveform")
.font(.subheadline.weight(.medium))
Spacer()
Text(memo.recordedAt, style: .time)
.font(.caption)
.foregroundStyle(.secondary)
}
if memo.isTranscribing {
HStack(spacing: 6) {
ProgressView()
.controlSize(.small)
Text(String(localized: "memo.transcribing"))
.font(.caption)
.foregroundStyle(.secondary)
}
} else if let transcript = memo.transcript {
Text(transcript.prefix(100))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
.padding(.vertical, 4)
.accessibilityElement(children: .combine)
}
}