From 2d6cf1795af653957fc990db6949b92659d67bf4 Mon Sep 17 00:00:00 2001 From: Alexey Haidamaka Date: Tue, 2 Sep 2025 13:52:16 +0200 Subject: [PATCH] add prompt reordering for enhancement prompts --- VoiceInk/Views/EnhancementSettingsView.swift | 184 +++++++++++++++++-- 1 file changed, 165 insertions(+), 19 deletions(-) diff --git a/VoiceInk/Views/EnhancementSettingsView.swift b/VoiceInk/Views/EnhancementSettingsView.swift index 714d677..4026b5e 100644 --- a/VoiceInk/Views/EnhancementSettingsView.swift +++ b/VoiceInk/Views/EnhancementSettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import UniformTypeIdentifiers struct EnhancementSettingsView: View { @EnvironmentObject private var enhancementService: AIEnhancementService @@ -79,25 +80,22 @@ struct EnhancementSettingsView: View { Text("Enhancement Prompt") .font(.headline) - // Prompts Section - VStack(alignment: .leading, spacing: 12) { - PromptSelectionGrid( - prompts: enhancementService.allPrompts, - selectedPromptId: enhancementService.selectedPromptId, - onPromptSelected: { prompt in - enhancementService.setActivePrompt(prompt) - }, - onEditPrompt: { prompt in - selectedPromptForEdit = prompt - }, - onDeletePrompt: { prompt in - enhancementService.deletePrompt(prompt) - }, - onAddNewPrompt: { - isEditingPrompt = true - } - ) - } + // Reorderable prompts grid with drag-and-drop + ReorderablePromptGrid( + selectedPromptId: enhancementService.selectedPromptId, + onPromptSelected: { prompt in + enhancementService.setActivePrompt(prompt) + }, + onEditPrompt: { prompt in + selectedPromptForEdit = prompt + }, + onDeletePrompt: { prompt in + enhancementService.deletePrompt(prompt) + }, + onAddNewPrompt: { + isEditingPrompt = true + } + ) } .padding() .background(CardBackground(isSelected: false)) @@ -115,3 +113,151 @@ struct EnhancementSettingsView: View { } } } + +// MARK: - Drag & Drop Reorderable Grid +private struct ReorderablePromptGrid: View { + @EnvironmentObject private var enhancementService: AIEnhancementService + + let selectedPromptId: UUID? + let onPromptSelected: (CustomPrompt) -> Void + let onEditPrompt: ((CustomPrompt) -> Void)? + let onDeletePrompt: ((CustomPrompt) -> Void)? + let onAddNewPrompt: (() -> Void)? + + @State private var draggingItem: CustomPrompt? + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + if enhancementService.customPrompts.isEmpty { + Text("No prompts available") + .foregroundColor(.secondary) + .font(.caption) + } else { + let columns = [ + GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36) + ] + + LazyVGrid(columns: columns, spacing: 16) { + ForEach(enhancementService.customPrompts) { prompt in + prompt.promptIcon( + isSelected: selectedPromptId == prompt.id, + onTap: { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + onPromptSelected(prompt) + } + }, + onEdit: onEditPrompt, + onDelete: onDeletePrompt + ) + .opacity(draggingItem?.id == prompt.id ? 0.3 : 1.0) + .scaleEffect(draggingItem?.id == prompt.id ? 1.05 : 1.0) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke( + draggingItem != nil && draggingItem?.id != prompt.id + ? Color.accentColor.opacity(0.25) + : Color.clear, + lineWidth: 1 + ) + ) + .animation(.easeInOut(duration: 0.15), value: draggingItem?.id == prompt.id) + .onDrag { + draggingItem = prompt + return NSItemProvider(object: prompt.id.uuidString as NSString) + } + .onDrop( + of: [UTType.text], + delegate: PromptDropDelegate( + item: prompt, + prompts: $enhancementService.customPrompts, + draggingItem: $draggingItem + ) + ) + } + + if let onAddNewPrompt = onAddNewPrompt { + CustomPrompt.addNewButton { + onAddNewPrompt() + } + .help("Add new prompt") + .onDrop( + of: [UTType.text], + delegate: PromptEndDropDelegate( + prompts: $enhancementService.customPrompts, + draggingItem: $draggingItem + ) + ) + } + } + .padding(.vertical, 12) + .padding(.horizontal, 16) + + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.secondary) + + Text("Double-click to edit • Right-click for more options") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.top, 8) + .padding(.horizontal, 16) + } + } + } +} + +// MARK: - Drop Delegates +private struct PromptDropDelegate: DropDelegate { + let item: CustomPrompt + @Binding var prompts: [CustomPrompt] + @Binding var draggingItem: CustomPrompt? + + func dropEntered(info: DropInfo) { + guard let draggingItem = draggingItem, draggingItem != item else { return } + guard let fromIndex = prompts.firstIndex(of: draggingItem), + let toIndex = prompts.firstIndex(of: item) else { return } + + // Move item as you hover for immediate visual update + if prompts[toIndex].id != draggingItem.id { + withAnimation(.easeInOut(duration: 0.12)) { + let from = fromIndex + let to = toIndex + prompts.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to) + } + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + draggingItem = nil + return true + } +} + +private struct PromptEndDropDelegate: DropDelegate { + @Binding var prompts: [CustomPrompt] + @Binding var draggingItem: CustomPrompt? + + func validateDrop(info: DropInfo) -> Bool { true } + func dropUpdated(info: DropInfo) -> DropProposal? { DropProposal(operation: .move) } + + func performDrop(info: DropInfo) -> Bool { + guard let draggingItem = draggingItem, + let currentIndex = prompts.firstIndex(of: draggingItem) else { + self.draggingItem = nil + return false + } + + // Move to end if dropped on the trailing "Add New" tile + withAnimation(.easeInOut(duration: 0.12)) { + prompts.move(fromOffsets: IndexSet(integer: currentIndex), toOffset: prompts.endIndex) + } + self.draggingItem = nil + return true + } +}