Refactor word replacements with inline UI
This commit is contained in:
parent
a363745f36
commit
01c12cdd2d
@ -109,6 +109,10 @@ struct VocabularyView: View {
|
|||||||
UserDefaults.standard.set(sortMode.rawValue, forKey: "vocabularySortMode")
|
UserDefaults.standard.set(sortMode.rawValue, forKey: "vocabularySortMode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var shouldShowAddButton: Bool {
|
||||||
|
!newWord.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
GroupBox {
|
GroupBox {
|
||||||
@ -129,16 +133,19 @@ struct VocabularyView: View {
|
|||||||
.font(.system(size: 13))
|
.font(.system(size: 13))
|
||||||
.onSubmit { addWords() }
|
.onSubmit { addWords() }
|
||||||
|
|
||||||
Button(action: addWords) {
|
if shouldShowAddButton {
|
||||||
Image(systemName: "plus.circle.fill")
|
Button(action: addWords) {
|
||||||
.symbolRenderingMode(.hierarchical)
|
Image(systemName: "plus.circle.fill")
|
||||||
.foregroundStyle(.blue)
|
.symbolRenderingMode(.hierarchical)
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.foregroundStyle(.blue)
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.disabled(newWord.isEmpty)
|
||||||
|
.help("Add word")
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.disabled(newWord.isEmpty)
|
|
||||||
.help("Add word")
|
|
||||||
}
|
}
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: shouldShowAddButton)
|
||||||
|
|
||||||
if !vocabularyManager.items.isEmpty {
|
if !vocabularyManager.items.isEmpty {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
@ -214,7 +221,7 @@ struct VocabularyView: View {
|
|||||||
struct VocabularyWordView: View {
|
struct VocabularyWordView: View {
|
||||||
let item: VocabularyWord
|
let item: VocabularyWord
|
||||||
let onDelete: () -> Void
|
let onDelete: () -> Void
|
||||||
@State private var isHovered = false
|
@State private var isDeleteHovered = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
@ -228,14 +235,14 @@ struct VocabularyWordView: View {
|
|||||||
Button(action: onDelete) {
|
Button(action: onDelete) {
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark.circle.fill")
|
||||||
.symbolRenderingMode(.hierarchical)
|
.symbolRenderingMode(.hierarchical)
|
||||||
.foregroundStyle(isHovered ? .red : .secondary)
|
.foregroundStyle(isDeleteHovered ? .red : .secondary)
|
||||||
.contentTransition(.symbolEffect(.replace))
|
.contentTransition(.symbolEffect(.replace))
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.help("Remove word")
|
.help("Remove word")
|
||||||
.onHover { hover in
|
.onHover { hover in
|
||||||
withAnimation(.easeInOut(duration: 0.2)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
isHovered = hover
|
isDeleteHovered = hover
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,12 +93,13 @@ class WordReplacementManager: ObservableObject {
|
|||||||
|
|
||||||
struct WordReplacementView: View {
|
struct WordReplacementView: View {
|
||||||
@StateObject private var manager = WordReplacementManager()
|
@StateObject private var manager = WordReplacementManager()
|
||||||
@State private var showAddReplacementModal = false
|
|
||||||
@State private var showAlert = false
|
@State private var showAlert = false
|
||||||
@State private var editingOriginal: String? = nil
|
@State private var editingOriginal: String? = nil
|
||||||
|
|
||||||
@State private var alertMessage = ""
|
@State private var alertMessage = ""
|
||||||
@State private var sortMode: SortMode = .originalAsc
|
@State private var sortMode: SortMode = .originalAsc
|
||||||
|
@State private var originalWord = ""
|
||||||
|
@State private var replacementWord = ""
|
||||||
|
@State private var showInfoPopover = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if let savedSort = UserDefaults.standard.string(forKey: "wordReplacementSortMode"),
|
if let savedSort = UserDefaults.standard.string(forKey: "wordReplacementSortMode"),
|
||||||
@ -131,6 +132,10 @@ struct WordReplacementView: View {
|
|||||||
}
|
}
|
||||||
UserDefaults.standard.set(sortMode.rawValue, forKey: "wordReplacementSortMode")
|
UserDefaults.standard.set(sortMode.rawValue, forKey: "wordReplacementSortMode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var shouldShowAddButton: Bool {
|
||||||
|
!originalWord.isEmpty || !replacementWord.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
@ -141,296 +146,118 @@ struct WordReplacementView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
} icon: {
|
} icon: {
|
||||||
Image(systemName: "info.circle.fill")
|
Button(action: { showInfoPopover.toggle() }) {
|
||||||
.foregroundColor(.blue)
|
Image(systemName: "info.circle.fill")
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.popover(isPresented: $showInfoPopover) {
|
||||||
|
WordReplacementInfoPopover()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
HStack(spacing: 8) {
|
||||||
HStack(spacing: 16) {
|
TextField("Original text (use commas for multiple)", text: $originalWord)
|
||||||
Button(action: { toggleSort(for: .original) }) {
|
.textFieldStyle(.roundedBorder)
|
||||||
HStack(spacing: 4) {
|
.font(.system(size: 13))
|
||||||
Text("Original")
|
|
||||||
.font(.headline)
|
Image(systemName: "arrow.right")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
if sortMode == .originalAsc || sortMode == .originalDesc {
|
.font(.system(size: 10))
|
||||||
Image(systemName: sortMode == .originalAsc ? "chevron.up" : "chevron.down")
|
.frame(width: 10)
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.accentColor)
|
TextField("Replacement text", text: $replacementWord)
|
||||||
}
|
.textFieldStyle(.roundedBorder)
|
||||||
}
|
.font(.system(size: 13))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.onSubmit { addReplacement() }
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
if shouldShowAddButton {
|
||||||
|
Button(action: addReplacement) {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.borderless)
|
||||||
|
.disabled(originalWord.isEmpty || replacementWord.isEmpty)
|
||||||
Image(systemName: "arrow.right")
|
.help("Add word replacement")
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.frame(width: 20)
|
|
||||||
|
|
||||||
Button(action: { toggleSort(for: .replacement) }) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text("Replacement")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
if sortMode == .replacementAsc || sortMode == .replacementDesc {
|
|
||||||
Image(systemName: sortMode == .replacementAsc ? "chevron.up" : "chevron.down")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Button(action: { showAddReplacementModal = true }) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
}
|
|
||||||
.frame(width: 60)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
}
|
||||||
.padding(.vertical, 8)
|
.animation(.easeInOut(duration: 0.2), value: shouldShowAddButton)
|
||||||
.background(Color(.controlBackgroundColor))
|
|
||||||
|
if !manager.replacements.isEmpty {
|
||||||
Divider()
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
// Content
|
Button(action: { toggleSort(for: .original) }) {
|
||||||
if manager.replacements.isEmpty {
|
HStack(spacing: 4) {
|
||||||
EmptyStateView(showAddModal: $showAddReplacementModal)
|
Text("Original")
|
||||||
} else {
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if sortMode == .originalAsc || sortMode == .originalDesc {
|
||||||
|
Image(systemName: sortMode == .originalAsc ? "chevron.up" : "chevron.down")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Sort by original")
|
||||||
|
|
||||||
|
Image(systemName: "arrow.right")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.frame(width: 10)
|
||||||
|
|
||||||
|
Button(action: { toggleSort(for: .replacement) }) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text("Replacement")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
if sortMode == .replacementAsc || sortMode == .replacementDesc {
|
||||||
|
Image(systemName: sortMode == .replacementAsc ? "chevron.up" : "chevron.down")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help("Sort by replacement")
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(Array(sortedReplacements.enumerated()), id: \.offset) { index, pair in
|
ForEach(sortedReplacements, id: \.key) { pair in
|
||||||
ReplacementRow(
|
ReplacementRow(
|
||||||
original: pair.key,
|
original: pair.key,
|
||||||
replacement: pair.value,
|
replacement: pair.value,
|
||||||
onDelete: { manager.removeReplacement(original: pair.key) },
|
onDelete: { manager.removeReplacement(original: pair.key) },
|
||||||
onEdit: { editingOriginal = pair.key }
|
onEdit: { editingOriginal = pair.key }
|
||||||
)
|
)
|
||||||
|
|
||||||
if index != sortedReplacements.count - 1 {
|
if pair.key != sortedReplacements.last?.key {
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.leading, 32)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color(.controlBackgroundColor))
|
|
||||||
}
|
}
|
||||||
|
.frame(maxHeight: 300)
|
||||||
}
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.sheet(isPresented: $showAddReplacementModal) {
|
|
||||||
AddReplacementSheet(manager: manager)
|
|
||||||
}
|
|
||||||
// Edit existing replacement
|
|
||||||
.sheet(item: $editingOriginal) { original in
|
.sheet(item: $editingOriginal) { original in
|
||||||
EditReplacementSheet(manager: manager, originalKey: original)
|
EditReplacementSheet(manager: manager, originalKey: original)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EmptyStateView: View {
|
|
||||||
@Binding var showAddModal: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
Image(systemName: "text.word.spacing")
|
|
||||||
.font(.system(size: 32))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("No Replacements")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Text("Add word replacements to automatically replace text.")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.frame(maxWidth: 250)
|
|
||||||
|
|
||||||
Button("Add Replacement") {
|
|
||||||
showAddModal = true
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.regular)
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct AddReplacementSheet: View {
|
|
||||||
@ObservedObject var manager: WordReplacementManager
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@State private var originalWord = ""
|
|
||||||
@State private var replacementWord = ""
|
|
||||||
@State private var showAlert = false
|
|
||||||
@State private var alertMessage = ""
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// Header
|
|
||||||
HStack {
|
|
||||||
Button("Cancel", role: .cancel) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.keyboardShortcut(.escape, modifiers: [])
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("Add Word Replacement")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button("Add") {
|
|
||||||
addReplacement()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.small)
|
|
||||||
.disabled(originalWord.isEmpty || replacementWord.isEmpty)
|
|
||||||
.keyboardShortcut(.return, modifiers: [])
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.background(CardBackground(isSelected: false))
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
// Description
|
|
||||||
Text("Define a word or phrase to be automatically replaced.")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top, 8)
|
|
||||||
|
|
||||||
// Form Content
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
// Original Text Section
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
HStack {
|
|
||||||
Text("Original Text")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text("Required")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("Enter word or phrase to replace (use commas for multiple)", text: $originalWord)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.font(.body)
|
|
||||||
Text("Separate multiple originals with commas, e.g. Voicing, Voice ink, Voiceing")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
// Replacement Text Section
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
HStack {
|
|
||||||
Text("Replacement Text")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text("Required")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditor(text: $replacementWord)
|
|
||||||
.font(.body)
|
|
||||||
.frame(height: 100)
|
|
||||||
.padding(8)
|
|
||||||
.background(Color(.textBackgroundColor))
|
|
||||||
.cornerRadius(6)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.stroke(Color(.separatorColor), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example Section
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Examples")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
// Single original -> replacement
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Original:")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("my website link")
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(systemName: "arrow.right")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Replacement:")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("https://tryvoiceink.com")
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(12)
|
|
||||||
.background(Color(.textBackgroundColor))
|
|
||||||
.cornerRadius(8)
|
|
||||||
|
|
||||||
// Comma-separated originals -> single replacement
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Original:")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("Voicing, Voice ink, Voiceing")
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(systemName: "arrow.right")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Replacement:")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text("VoiceInk")
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(12)
|
|
||||||
.background(Color(.textBackgroundColor))
|
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.top, 8)
|
|
||||||
}
|
|
||||||
.padding(.vertical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 460, height: 520)
|
|
||||||
.alert("Word Replacement", isPresented: $showAlert) {
|
.alert("Word Replacement", isPresented: $showAlert) {
|
||||||
Button("OK", role: .cancel) {}
|
Button("OK", role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
@ -440,7 +267,7 @@ struct AddReplacementSheet: View {
|
|||||||
|
|
||||||
private func addReplacement() {
|
private func addReplacement() {
|
||||||
let original = originalWord.trimmingCharacters(in: .whitespacesAndNewlines)
|
let original = originalWord.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let replacement = replacementWord
|
let replacement = replacementWord.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
let tokens = original
|
let tokens = original
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
@ -450,7 +277,8 @@ struct AddReplacementSheet: View {
|
|||||||
|
|
||||||
let result = manager.addReplacement(original: original, replacement: replacement)
|
let result = manager.addReplacement(original: original, replacement: replacement)
|
||||||
if result.success {
|
if result.success {
|
||||||
dismiss()
|
originalWord = ""
|
||||||
|
replacementWord = ""
|
||||||
} else {
|
} else {
|
||||||
if let conflictingWord = result.conflictingWord {
|
if let conflictingWord = result.conflictingWord {
|
||||||
alertMessage = "'\(conflictingWord)' already exists in word replacements"
|
alertMessage = "'\(conflictingWord)' already exists in word replacements"
|
||||||
@ -462,68 +290,150 @@ struct AddReplacementSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct WordReplacementInfoPopover: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("How to use Word Replacements")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Separate multiple originals with commas:")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text("Voicing, Voice ink, Voiceing")
|
||||||
|
.font(.callout)
|
||||||
|
.padding(8)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color(.textBackgroundColor))
|
||||||
|
.cornerRadius(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
Text("Examples")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Original:")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("my website link")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: "arrow.right")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Replacement:")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("https://tryvoiceink.com")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color(.textBackgroundColor))
|
||||||
|
.cornerRadius(6)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Original:")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("Voicing, Voice ink")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: "arrow.right")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Replacement:")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text("VoiceInk")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color(.textBackgroundColor))
|
||||||
|
.cornerRadius(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(width: 380)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ReplacementRow: View {
|
struct ReplacementRow: View {
|
||||||
let original: String
|
let original: String
|
||||||
let replacement: String
|
let replacement: String
|
||||||
let onDelete: () -> Void
|
let onDelete: () -> Void
|
||||||
let onEdit: () -> Void
|
let onEdit: () -> Void
|
||||||
|
@State private var isEditHovered = false
|
||||||
|
@State private var isDeleteHovered = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 8) {
|
||||||
// Original Text Container
|
Text(original)
|
||||||
HStack {
|
.font(.system(size: 13))
|
||||||
Text(original)
|
.lineLimit(2)
|
||||||
.font(.body)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.lineLimit(2)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Color(.textBackgroundColor))
|
|
||||||
.cornerRadius(6)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
// Arrow
|
|
||||||
Image(systemName: "arrow.right")
|
Image(systemName: "arrow.right")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.font(.system(size: 12))
|
.font(.system(size: 10))
|
||||||
|
.frame(width: 10)
|
||||||
// Replacement Text Container
|
|
||||||
HStack {
|
ZStack(alignment: .trailing) {
|
||||||
Text(replacement)
|
Text(replacement)
|
||||||
.font(.body)
|
.font(.system(size: 13))
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.horizontal, 12)
|
.padding(.trailing, 50)
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Color(.textBackgroundColor))
|
HStack(spacing: 6) {
|
||||||
.cornerRadius(6)
|
Button(action: onEdit) {
|
||||||
|
Image(systemName: "pencil.circle.fill")
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.foregroundColor(isEditHovered ? .accentColor : .secondary)
|
||||||
|
.contentTransition(.symbolEffect(.replace))
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.help("Edit replacement")
|
||||||
|
.onHover { hover in
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
isEditHovered = hover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: onDelete) {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.foregroundStyle(isDeleteHovered ? .red : .secondary)
|
||||||
|
.contentTransition(.symbolEffect(.replace))
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.help("Remove replacement")
|
||||||
|
.onHover { hover in
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
isDeleteHovered = hover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
// Edit Button
|
|
||||||
Button(action: onEdit) {
|
|
||||||
Image(systemName: "pencil.circle.fill")
|
|
||||||
.symbolRenderingMode(.hierarchical)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
.font(.system(size: 16))
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.help("Edit replacement")
|
|
||||||
|
|
||||||
// Delete Button
|
|
||||||
Button(action: onDelete) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.symbolRenderingMode(.hierarchical)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.font(.system(size: 16))
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderless)
|
|
||||||
.help("Remove replacement")
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.contentShape(Rectangle())
|
.padding(.horizontal, 4)
|
||||||
.background(Color(.controlBackgroundColor))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user