131 lines
4.9 KiB
Swift
131 lines
4.9 KiB
Swift
import AppKit
|
|
import SstCore
|
|
import SstData
|
|
import SstUI
|
|
import SwiftUI
|
|
|
|
private let dutch = Locale(identifier: "nl_NL")
|
|
|
|
struct HistoryView: View {
|
|
@EnvironmentObject private var model: AppModel
|
|
@ObservedObject var list: HistoryViewModel
|
|
@FocusState private var searchFocused: Bool
|
|
@FocusState private var listFocused: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: Layout.panelGap) {
|
|
SearchField(text: $list.query, focused: $searchFocused)
|
|
ZStack {
|
|
PaperBackground()
|
|
content
|
|
}
|
|
}
|
|
.onChange(of: model.searchFocusTick) { _, _ in searchFocused = true }
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
if list.days.isEmpty {
|
|
Text(emptyMessage)
|
|
.textStyle(Typography.body)
|
|
.foregroundStyle(Palette.inkSecondary)
|
|
.padding(Layout.panelPadding)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
} else {
|
|
ScrollList {
|
|
ForEach(list.days) { day in
|
|
Section {
|
|
ForEach(day.entries) { entry in
|
|
HistoryRow(entry: entry, isSelected: list.selection == entry.id, list: list)
|
|
}
|
|
} header: {
|
|
SilkLabel(day.day.formatted(Date.FormatStyle(date: .complete, time: .omitted).locale(dutch)))
|
|
.padding(.horizontal, Space.s4)
|
|
.padding(.vertical, Space.s3)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Palette.surfacePaper)
|
|
}
|
|
}
|
|
}
|
|
.focusable()
|
|
.focused($listFocused)
|
|
.focusEffectDisabled()
|
|
.onChange(of: list.selection) { _, new in if new != nil { listFocused = true } }
|
|
.onDeleteCommand {
|
|
if let id = list.selection { list.delete(id: id) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private var emptyMessage: String {
|
|
if list.totalCount > 0 { return Strings.noResults }
|
|
return model.settings.keepHistory ? Strings.noTranscripts : Strings.historyOff
|
|
}
|
|
}
|
|
|
|
private struct HistoryRow: View {
|
|
let entry: HistoryEntry
|
|
let isSelected: Bool
|
|
@ObservedObject var list: HistoryViewModel
|
|
|
|
var body: some View {
|
|
HStack(alignment: .firstTextBaseline, spacing: Space.s4) {
|
|
Text(entry.date.formatted(Date.FormatStyle().hour().minute().second().locale(dutch)))
|
|
.textStyle(Typography.counterSmall)
|
|
.foregroundStyle(Palette.inkSecondary)
|
|
VStack(alignment: .leading, spacing: Space.s2) {
|
|
Text(markedText)
|
|
.textStyle(Typography.reading)
|
|
.foregroundStyle(Palette.inkPrimary)
|
|
.textSelection(.enabled)
|
|
if !entry.corrections.isEmpty {
|
|
SilkLabel(correctionSummary, style: Typography.labelSmall)
|
|
}
|
|
if isSelected && entry.rawText != entry.finalText {
|
|
Text("\(Strings.original): \(entry.rawText)")
|
|
.textStyle(Typography.meta)
|
|
.foregroundStyle(Palette.inkSecondary)
|
|
}
|
|
}
|
|
Spacer(minLength: Space.s4)
|
|
Button(Strings.copy) { copy(entry.finalText) }
|
|
.buttonStyle(KeyButtonStyle())
|
|
}
|
|
.padding(.horizontal, Space.s4)
|
|
.padding(.vertical, Space.s3)
|
|
.frame(minHeight: Size.rowHeight)
|
|
.background(isSelected ? Palette.surfaceSelection : Color.clear)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture { list.selection = entry.id }
|
|
.contextMenu {
|
|
Button(Strings.copy) { copy(entry.finalText) }
|
|
Button(Strings.copyOriginal) { copy(entry.rawText) }
|
|
Divider()
|
|
Button(Strings.delete, role: .destructive) { list.delete(id: entry.id) }
|
|
}
|
|
}
|
|
|
|
/// The final text with every dictionary replacement highlighted.
|
|
private var markedText: AttributedString {
|
|
var result = AttributedString()
|
|
for segment in CorrectionMarkup.segments(finalText: entry.finalText, corrections: entry.corrections) {
|
|
var piece = AttributedString(segment.text)
|
|
if segment.correction != nil {
|
|
piece.backgroundColor = Palette.levelAmber.opacity(Opacity.mark)
|
|
}
|
|
result += piece
|
|
}
|
|
return result
|
|
}
|
|
|
|
private var correctionSummary: String {
|
|
let changes = entry.corrections.map { "\($0.heard) → \($0.wrote)" }.joined(separator: ", ")
|
|
return "\(Strings.corrections(entry.corrections.count)) · \(changes)"
|
|
}
|
|
|
|
private func copy(_ text: String) {
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(text, forType: .string)
|
|
}
|
|
}
|