diff --git a/Promptly.xcodeproj/project.pbxproj b/Promptly.xcodeproj/project.pbxproj index 5ab0761..512b324 100644 --- a/Promptly.xcodeproj/project.pbxproj +++ b/Promptly.xcodeproj/project.pbxproj @@ -419,7 +419,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Promptly/Promptly.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 8Y3J97SYZG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -444,7 +444,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 15.5; - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.6; PRODUCT_BUNDLE_IDENTIFIER = com.urbanmechanicsltd.Promptly; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -464,7 +464,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Promptly/Promptly.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 8Y3J97SYZG; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -489,7 +489,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 15.5; - MARKETING_VERSION = 1.0.5; + MARKETING_VERSION = 1.0.6; PRODUCT_BUNDLE_IDENTIFIER = com.urbanmechanicsltd.Promptly; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; diff --git a/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate b/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate index 67e4c5f..0ff029e 100644 Binary files a/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate and b/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Promptly/PromptlyApp.swift b/Promptly/PromptlyApp.swift index 42ff366..c0da02d 100644 --- a/Promptly/PromptlyApp.swift +++ b/Promptly/PromptlyApp.swift @@ -53,7 +53,7 @@ extension PromptlyApp: WhatsNewCollectionProvider { /// A WhatsNewCollection var whatsNewCollection: WhatsNewCollection { WhatsNew( - version: "1.0.5", + version: "1.0.6", title: "DSMPrompt", features: [ .init( @@ -61,8 +61,16 @@ extension PromptlyApp: WhatsNewCollectionProvider { systemName: "hammer", foregroundColor: .gray ), - title: "Bug Fixes", - subtitle: "Bug fixes and stability improvements." + title: "Bug Fixes and reverted features!", + subtitle: "Whoops! We fixed all the issues in the latest version." + ), + .init( + image: .init( + systemName: "hammer", + foregroundColor: .gray + ), + title: "Still has exports / imports", + subtitle: "Only the scripts changed. Essentially - 1.0.4 + 1.0.3 = 1.0.6?" ) ], primaryAction: .init( diff --git a/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift b/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift index 522af2d..6893677 100644 --- a/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift +++ b/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift @@ -14,12 +14,6 @@ class RefreshTrigger: ObservableObject { } } -struct LineGroup: Identifiable { - let id = UUID() - let section: ScriptSection? - let lines: [ScriptLine] -} - struct EditScriptView: View { @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss @@ -48,9 +42,6 @@ struct EditScriptView: View { @State private var lineBeingFlagged: ScriptLine? @StateObject private var refreshTrigger = RefreshTrigger() - @State private var lineGroups: [LineGroup] = [] - - var sortedLines: [ScriptLine] { script.lines.sorted { $0.lineNumber < $1.lineNumber } } @@ -129,7 +120,7 @@ struct EditScriptView: View { } } .sheet(isPresented: $showingFlagEditor) { - if selectedLines.count == 1, let line = selectedLinesArray.first { + if selectedLines.count == 1, let line = lineBeingFlagged { FlagEditorView(line: line) { try? modelContext.save() refreshTrigger.refresh() @@ -198,9 +189,6 @@ struct EditScriptView: View { } .navigationTitle(Text(script.name)) .navigationBarTitleDisplayMode(.inline) - .task { - lineGroups = await groupLinesBySection(lines: sortedLines, sections: sortedSections) - } } // MARK: - View Components @@ -293,139 +281,42 @@ struct EditScriptView: View { } private var scriptContentView: some View { - ScriptTableView( - lineGroups: lineGroups, - isEditing: isEditing, - selectedLines: selectedLines, - editingLineId: editingLineId, - isSelectingForSection: isSelectingLineForSection, - editingText: $editingText, - onToggleSelection: { selectedLine in - if isSelectingLineForSection { - selectedLineForSection = selectedLine - showingLineConfirmation = true - isSelectingLineForSection = false - } else { - toggleLineSelection(selectedLine) - } - }, - onStartTextEdit: { lineToEdit in - startEditing(line: lineToEdit) - }, - onFinishTextEdit: { newText in - finishEditing(newText: newText) - }, - onInsertAfter: { lineToInsertAfter in - insertAfterLineNumber = lineToInsertAfter.lineNumber - showingAddLine = true - }, - onEditFlags: { lineToFlag in - lineBeingFlagged = lineToFlag - showingFlagEditor = true - }, - onSectionTap: { section in - print("sec tap") - // dismiss() - // NotificationCenter.default.post( - // name: NSNotification.Name("ScrollToSection"), - // object: section.id - // ) - } - ) - } - - // MARK: - Actions - - private func groupLinesBySection(lines: [ScriptLine], sections: [ScriptSection]) async -> [LineGroup] { - guard !lines.isEmpty else { return [] } - guard !sections.isEmpty else { - return [LineGroup(section: nil, lines: lines)] - } - - let sectionRanges = await withTaskGroup(of: (Int, Int, Int).self) { group in - for (index, section) in sections.enumerated() { - group.addTask { - let startLine = section.startLineNumber - let endLine = (index + 1 < sections.count) ? - sections[index + 1].startLineNumber - 1 : - lines.last?.lineNumber ?? startLine - return (index, startLine, endLine) + ScrollView { + LazyVStack(spacing: 8) { + ForEach(sortedLines, id: \.id) { line in + EditableScriptLineView( + line: line, + isEditing: isEditing, + isSelected: selectedLines.contains(line.id), + isEditingText: editingLineId == line.id, + isSelectingForSection: isSelectingLineForSection, + editingText: $editingText + ) { selectedLine in + if isSelectingLineForSection { + selectedLineForSection = selectedLine + showingLineConfirmation = true + isSelectingLineForSection = false + } else { + toggleLineSelection(selectedLine) + } + } onStartTextEdit: { lineToEdit in + startEditing(line: lineToEdit) + } onFinishTextEdit: { newText in + finishEditing(newText: newText) + } onInsertAfter: { lineToInsertAfter in + insertAfterLineNumber = lineToInsertAfter.lineNumber + showingAddLine = true + } onEditFlags: { lineToFlag in + lineBeingFlagged = lineToFlag + showingFlagEditor = true + } } } - - var ranges: [(Int, Int, Int)] = [] - for await range in group { - ranges.append(range) - } - return ranges.sorted { $0.0 < $1.0 } - } - - var groups: [LineGroup] = [] - var processedLines = Set() - - for (index, startLine, endLine) in sectionRanges { - let section = sections[index] - - let startIdx = binarySearchStart(lines: lines, lineNumber: startLine) - let endIdx = binarySearchEnd(lines: lines, lineNumber: endLine) - - guard startIdx < lines.count && endIdx >= 0 else { continue } - - let sectionLines = lines[startIdx...min(endIdx, lines.count - 1)] - .filter { !processedLines.contains($0.lineNumber) } - - if !sectionLines.isEmpty { - groups.append(LineGroup(section: section, lines: Array(sectionLines))) - processedLines.formUnion(sectionLines.map { $0.lineNumber }) - } - } - - let ungroupedLines = lines.filter { !processedLines.contains($0.lineNumber) } - if !ungroupedLines.isEmpty { - groups.append(LineGroup(section: nil, lines: ungroupedLines)) - } - - return groups.sorted { - ($0.lines.first?.lineNumber ?? 0) < ($1.lines.first?.lineNumber ?? 0) + .padding() } } - private func binarySearchStart(lines: [ScriptLine], lineNumber: Int) -> Int { - var left = 0, right = lines.count - 1 - while left <= right { - let mid = (left + right) / 2 - if lines[mid].lineNumber >= lineNumber { - right = mid - 1 - } else { - left = mid + 1 - } - } - return left - } - - private func binarySearchEnd(lines: [ScriptLine], lineNumber: Int) -> Int { - var left = 0, right = lines.count - 1 - while left <= right { - let mid = (left + right) / 2 - if lines[mid].lineNumber <= lineNumber { - left = mid + 1 - } else { - right = mid - 1 - } - } - return right - } - - private func updateSectionsAfterLineChange(at changePoint: Int, delta: Int) { - script.sections.forEach { section in - if section.startLineNumber > changePoint { - section.startLineNumber += delta - } - if let endLine = section.endLineNumber, endLine > changePoint { - section.endLineNumber! += delta - } - } - } + // MARK: - Actions private func toggleLineSelection(_ line: ScriptLine) { if selectedLines.contains(line.id) { @@ -466,48 +357,49 @@ struct EditScriptView: View { } private func deleteSelectedLines() { - let sortedLinesToDelete = linesToDelete.sorted { $0.lineNumber < $1.lineNumber } - let firstDeletedLineNumber = sortedLinesToDelete.first?.lineNumber ?? 0 - let deletedCount = sortedLinesToDelete.count + let linesToRemove = selectedLines.compactMap { id in + script.lines.first { $0.id == id } + } - for line in linesToDelete { + for line in linesToRemove { script.lines.removeAll { $0.id == line.id } + modelContext.delete(line) } - updateSectionsAfterLineChange(at: firstDeletedLineNumber, delta: -deletedCount) - - renumberLines() - try? modelContext.save() - selectedLines.removeAll() linesToDelete.removeAll() + renumberLines() + try? modelContext.save() } private func combineSelectedLines() { - let sortedSelected = selectedLinesArray.sorted { $0.lineNumber < $1.lineNumber } - guard let firstLine = sortedSelected.first else { return } + let linesToCombine = selectedLines.compactMap { id in + script.lines.first { $0.id == id } + }.sorted { $0.lineNumber < $1.lineNumber } + + guard let firstLine = linesToCombine.first else { return } + + let combinedContent = linesToCombine.map { $0.content }.joined(separator: " ") + let allCues = linesToCombine.flatMap { $0.cues } + let allFlags = Array(Set(linesToCombine.flatMap { $0.flags })) - let combinedContent = sortedSelected.map { $0.content }.joined(separator: " ") - let combinedCues = sortedSelected.flatMap { $0.cues } - let combinedFlags = Array(Set(sortedSelected.flatMap { $0.flags })) + firstLine.content = combinedContent + firstLine.flags = allFlags + firstLine.parseContentIntoElements() - let linesToRemove = sortedSelected.dropFirst() - let removedCount = linesToRemove.count + for cue in allCues { + cue.lineId = firstLine.id + } + let linesToRemove = Array(linesToCombine.dropFirst()) for line in linesToRemove { script.lines.removeAll { $0.id == line.id } + modelContext.delete(line) } - firstLine.content = combinedContent - firstLine.cues = combinedCues - firstLine.flags = combinedFlags - - updateSectionsAfterLineChange(at: firstLine.lineNumber, delta: -removedCount) - + selectedLines.removeAll() renumberLines() try? modelContext.save() - - selectedLines.removeAll() } private func createSectionWithSelectedLine() { @@ -705,7 +597,7 @@ struct EditableScriptLineView: View { private var backgroundOpacity: Double { if isSelected && isEditing { - return 1.0 + return 0.6 } else if line.isMarked { return 0.3 } else { @@ -1154,15 +1046,6 @@ struct AddLineView: View { line.lineNumber += 1 } - script.sections.forEach { section in - if section.startLineNumber >= newLineNumber { - section.startLineNumber += 1 - } - if let endLine = section.endLineNumber, endLine >= newLineNumber { - section.endLineNumber! += 1 - } - } - let newLine = ScriptLine( id: UUID(), lineNumber: newLineNumber, @@ -1178,14 +1061,29 @@ struct AddLineView: View { } private func iconForFlag(_ flag: ScriptLineFlags) -> String { - return flag.icon + switch flag { + case .stageDirection: + return "theatermasks" + case .skip: + return "forward.fill" + } } private func labelForFlag(_ flag: ScriptLineFlags) -> String { - return flag.label + switch flag { + case .stageDirection: + return "Stage" + case .skip: + return "Skip" + } } private func colorForFlag(_ flag: ScriptLineFlags) -> Color { - return flag.color + switch flag { + case .stageDirection: + return .purple + case .skip: + return .red + } } } diff --git a/Promptly/Views/Scripts/ScriptEditorView.swift b/Promptly/Views/Scripts/ScriptEditorView.swift index 4e849f0..dd84e28 100644 --- a/Promptly/Views/Scripts/ScriptEditorView.swift +++ b/Promptly/Views/Scripts/ScriptEditorView.swift @@ -1,5 +1,5 @@ // -// ScriptEditorView_v2.swift +// ScriptEditorView.swift // Promptly // // Created by Sasha Bagrov on 04/06/2025. @@ -21,10 +21,10 @@ struct ScriptEditorView: View { @State private var editingText: String = "" @State private var showingDeleteCueAlert = false @State private var cueToDelete: Cue? - @State private var sortedLines: [ScriptLine] = [] - @State private var sortedSections: [ScriptSection] = [] - @State private var lineGroups: [LineGroup] = [] - @State private var isProcessingGroups = false + + private var sortedLines: [ScriptLine] { + script.lines.sorted(by: { $0.lineNumber < $1.lineNumber }) + } private func handleCueEdit(cue: Cue) { guard let line = script.lines.first(where: { $0.id == cue.lineId }) else { return } @@ -33,121 +33,9 @@ struct ScriptEditorView: View { isShowingCueEditor = true } - private func setupSortedData() { - Task.detached(priority: .userInitiated) { - let lines = script.lines.sorted { $0.lineNumber < $1.lineNumber } - let sections = script.sections.sorted { $0.startLineNumber < $1.startLineNumber } - let groups = await groupLinesBySection(lines: lines, sections: sections) - - await MainActor.run { - self.sortedLines = lines - self.sortedSections = sections - self.lineGroups = groups - self.isProcessingGroups = false - } - } - } - - private func groupLinesBySection(lines: [ScriptLine], sections: [ScriptSection]) async -> [LineGroup] { - guard !lines.isEmpty else { return [] } - guard !sections.isEmpty else { - return [LineGroup(section: nil, lines: lines)] - } - - let sectionRanges = await withTaskGroup(of: (Int, Int, Int).self) { group in - for (index, section) in sections.enumerated() { - group.addTask { - let startLine = section.startLineNumber - let endLine = (index + 1 < sections.count) ? - sections[index + 1].startLineNumber - 1 : - lines.last?.lineNumber ?? startLine - return (index, startLine, endLine) - } - } - - var ranges: [(Int, Int, Int)] = [] - for await range in group { - ranges.append(range) - } - return ranges.sorted { $0.0 < $1.0 } - } - - var groups: [LineGroup] = [] - var processedLines = Set() - - for (index, startLine, endLine) in sectionRanges { - let section = sections[index] - - let startIdx = binarySearchStart(lines: lines, lineNumber: startLine) - let endIdx = binarySearchEnd(lines: lines, lineNumber: endLine) - - guard startIdx < lines.count && endIdx >= 0 else { continue } - - let sectionLines = lines[startIdx...min(endIdx, lines.count - 1)] - .filter { !processedLines.contains($0.lineNumber) } - - if !sectionLines.isEmpty { - groups.append(LineGroup(section: section, lines: Array(sectionLines))) - processedLines.formUnion(sectionLines.map { $0.lineNumber }) - } - } - - let ungroupedLines = lines.filter { !processedLines.contains($0.lineNumber) } - if !ungroupedLines.isEmpty { - groups.append(LineGroup(section: nil, lines: ungroupedLines)) - } - - return groups.sorted { - ($0.lines.first?.lineNumber ?? 0) < ($1.lines.first?.lineNumber ?? 0) - } - } - - private func binarySearchStart(lines: [ScriptLine], lineNumber: Int) -> Int { - var left = 0, right = lines.count - 1 - while left <= right { - let mid = (left + right) / 2 - if lines[mid].lineNumber >= lineNumber { - right = mid - 1 - } else { - left = mid + 1 - } - } - return left - } - - private func binarySearchEnd(lines: [ScriptLine], lineNumber: Int) -> Int { - var left = 0, right = lines.count - 1 - while left <= right { - let mid = (left + right) / 2 - if lines[mid].lineNumber <= lineNumber { - left = mid + 1 - } else { - right = mid - 1 - } - } - return right - } - var body: some View { VStack(spacing: 0) { - if isProcessingGroups { - ProgressView("Organizing script...") - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - scriptContentView - } - } - .onAppear { - isProcessingGroups = true - setupSortedData() - } - .onChange(of: script.sections.count) { _, _ in - isProcessingGroups = true - setupSortedData() - } - .onChange(of: script.lines.count) { _, _ in - isProcessingGroups = true - setupSortedData() + scriptContentView } .sheet(isPresented: $isShowingCueEditor) { if let line = selectedLine, let element = selectedElement { @@ -180,26 +68,33 @@ struct ScriptEditorView: View { } private var scriptContentView: some View { - ScriptTableView( - lineGroups: lineGroups, - selectedLine: selectedLine, - editingLineId: editingLineId, - editingText: $editingText, - onElementTap: { element, line in - handleElementTap(element: element, line: line) - }, - onLineTap: { line in - handleLineTap(line: line) - }, - onEditComplete: { line, newText in - updateLineContent(line: line, newText: newText) - }, - onCueDelete: handleCueDelete, - onCueEdit: handleCueEdit, - onSectionTap: { section in - + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + ForEach(sortedLines, id: \.id) { line in + ScriptLineView( + line: line, + isSelected: selectedLine?.id == line.id, + isEditing: editingLineId == line.id, + editingText: $editingText, + onElementTap: { element in + handleElementTap(element: element, line: line) + }, + onLineTap: { + handleLineTap(line: line) + }, + onEditComplete: { newText in + updateLineContent(line: line, newText: newText) + }, + onCueDelete: handleCueDelete, + onCueEdit: handleCueEdit + ) + .id("line-\(line.id)") + } + } + .padding() } - ) + } } private func handleElementTap(element: LineElement, line: ScriptLine) { @@ -223,8 +118,6 @@ struct ScriptEditorView: View { line.parseContentIntoElements() editingLineId = nil try? modelContext.save() - isProcessingGroups = true - setupSortedData() } private func handleCueDelete(cue: Cue) { diff --git a/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift b/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift deleted file mode 100644 index c395ba6..0000000 --- a/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift +++ /dev/null @@ -1,416 +0,0 @@ -// -// ScriptTableView.swift -// Promptly -// -// Created by Sasha Bagrov on 27/10/2025. -// - -import Foundation -import UIKit -import SwiftUI - -struct ScriptTableView: UIViewRepresentable { - let lineGroups: [LineGroup] - - // Edit mode properties - let isEditing: Bool - let selectedLines: Set - let editingLineId: UUID? - let isSelectingForSection: Bool - @Binding var editingText: String - - // Edit mode callbacks - let onToggleSelection: ((ScriptLine) -> Void)? - let onStartTextEdit: ((ScriptLine) -> Void)? - let onFinishTextEdit: ((String) -> Void)? - let onInsertAfter: ((ScriptLine) -> Void)? - let onEditFlags: ((ScriptLine) -> Void)? - let onSectionTap: ((ScriptSection) -> Void)? - - // Reading mode properties - let selectedLine: ScriptLine? - - // Reading mode callbacks - let onElementTap: ((LineElement, ScriptLine) -> Void)? - let onLineTap: ((ScriptLine) -> Void)? - let onEditComplete: ((ScriptLine, String) -> Void)? - let onCueDelete: ((Cue) -> Void)? - let onCueEdit: ((Cue) -> Void)? - - private let mode: TableMode - - enum TableMode { - case editing, reading - } - - // MARK: - Dual Initialisers - - // Edit mode initialiser (unchanged) - init( - lineGroups: [LineGroup], - isEditing: Bool, - selectedLines: Set, - editingLineId: UUID?, - isSelectingForSection: Bool, - editingText: Binding, - onToggleSelection: @escaping (ScriptLine) -> Void, - onStartTextEdit: @escaping (ScriptLine) -> Void, - onFinishTextEdit: @escaping (String) -> Void, - onInsertAfter: @escaping (ScriptLine) -> Void, - onEditFlags: @escaping (ScriptLine) -> Void, - onSectionTap: @escaping (ScriptSection) -> Void - ) { - self.lineGroups = lineGroups - self.isEditing = isEditing - self.selectedLines = selectedLines - self.editingLineId = editingLineId - self.isSelectingForSection = isSelectingForSection - self._editingText = editingText - self.onToggleSelection = onToggleSelection - self.onStartTextEdit = onStartTextEdit - self.onFinishTextEdit = onFinishTextEdit - self.onInsertAfter = onInsertAfter - self.onEditFlags = onEditFlags - self.onSectionTap = onSectionTap - - // Reading mode defaults - self.selectedLine = nil - self.onElementTap = nil - self.onLineTap = nil - self.onEditComplete = nil - self.onCueDelete = nil - self.onCueEdit = nil - - self.mode = .editing - } - - // Reading mode initialiser - init( - lineGroups: [LineGroup], - selectedLine: ScriptLine?, - editingLineId: UUID?, - editingText: Binding, - onElementTap: @escaping (LineElement, ScriptLine) -> Void, - onLineTap: @escaping (ScriptLine) -> Void, - onEditComplete: @escaping (ScriptLine, String) -> Void, - onCueDelete: @escaping (Cue) -> Void, - onCueEdit: @escaping (Cue) -> Void, - onSectionTap: @escaping (ScriptSection) -> Void - ) { - self.lineGroups = lineGroups - self.selectedLine = selectedLine - self.editingLineId = editingLineId - self._editingText = editingText - self.onElementTap = onElementTap - self.onLineTap = onLineTap - self.onEditComplete = onEditComplete - self.onCueDelete = onCueDelete - self.onCueEdit = onCueEdit - self.onSectionTap = onSectionTap - - // Edit mode defaults - self.isEditing = false - self.selectedLines = [] - self.isSelectingForSection = false - self.onToggleSelection = nil - self.onStartTextEdit = nil - self.onFinishTextEdit = nil - self.onInsertAfter = nil - self.onEditFlags = nil - - self.mode = .reading - } - - func makeUIView(context: Context) -> UITableView { - let tableView = UITableView() - tableView.delegate = context.coordinator - tableView.dataSource = context.coordinator - tableView.separatorStyle = .none - tableView.backgroundColor = UIColor.systemGroupedBackground - tableView.showsVerticalScrollIndicator = true - tableView.contentInset = UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0) - - // Register cells for both modes - tableView.register(HostingLineCell.self, forCellReuseIdentifier: "LineCell") - tableView.register(HostingReadingLineCell.self, forCellReuseIdentifier: "ReadingLineCell") - tableView.register(HostingSectionCell.self, forCellReuseIdentifier: "SectionCell") - - return tableView - } - - func updateUIView(_ uiView: UITableView, context: Context) { - context.coordinator.parent = self - uiView.reloadData() - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate { - var parent: ScriptTableView - - init(_ parent: ScriptTableView) { - self.parent = parent - } - - private var flattenedItems: [(type: ItemType, group: LineGroup, line: ScriptLine?)] { - var items: [(ItemType, LineGroup, ScriptLine?)] = [] - - for group in parent.lineGroups { - if let section = group.section { - items.append((.section, group, nil)) - } - for line in group.lines { - items.append((.line, group, line)) - } - } - return items - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return flattenedItems.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let item = flattenedItems[indexPath.row] - - switch item.type { - case .section: - let cell = tableView.dequeueReusableCell(withIdentifier: "SectionCell", for: indexPath) as! HostingSectionCell - cell.configure(with: item.group.section!, onTap: parent.onSectionTap!) - return cell - - case .line: - switch parent.mode { - case .editing: - let cell = tableView.dequeueReusableCell(withIdentifier: "LineCell", for: indexPath) as! HostingLineCell - cell.configure( - line: item.line!, - isEditing: parent.isEditing, - isSelected: parent.selectedLines.contains(item.line!.id), - isEditingText: parent.editingLineId == item.line!.id, - isSelectingForSection: parent.isSelectingForSection, - editingText: parent.editingText, - onToggleSelection: parent.onToggleSelection!, - onStartTextEdit: parent.onStartTextEdit!, - onFinishTextEdit: parent.onFinishTextEdit!, - onInsertAfter: parent.onInsertAfter!, - onEditFlags: parent.onEditFlags! - ) - return cell - - case .reading: - let cell = tableView.dequeueReusableCell(withIdentifier: "ReadingLineCell", for: indexPath) as! HostingReadingLineCell - cell.configure( - line: item.line!, - isSelected: parent.selectedLine?.id == item.line!.id, - isEditing: parent.editingLineId == item.line!.id, - editingText: parent.editingText, - onElementTap: { element in - self.parent.onElementTap?(element, item.line!) // - }, - onLineTap: { - self.parent.onLineTap?(item.line!) // - }, - onEditComplete: { newText in - self.parent.onEditComplete?(item.line!, newText) // - }, - onCueDelete: parent.onCueDelete!, - onCueEdit: parent.onCueEdit! - ) - return cell - } - } - } - - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - let item = flattenedItems[indexPath.row] - return item.type == .section ? 80 : 60 - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - } - - enum ItemType { - case section, line - } -} - -// UITableViewCell that hosts SwiftUI line content -class HostingLineCell: UITableViewCell { - private var hostingController: UIHostingController? - - override func prepareForReuse() { - super.prepareForReuse() - // Clean up when cell is reused - hostingController?.view.removeFromSuperview() - hostingController = nil - } - - func configure( - line: ScriptLine, - isEditing: Bool, - isSelected: Bool, - isEditingText: Bool, - isSelectingForSection: Bool, - editingText: String, - onToggleSelection: @escaping (ScriptLine) -> Void, - onStartTextEdit: @escaping (ScriptLine) -> Void, - onFinishTextEdit: @escaping (String) -> Void, - onInsertAfter: @escaping (ScriptLine) -> Void, - onEditFlags: @escaping (ScriptLine) -> Void - ) { - // Create binding for editingText - let editingTextBinding = Binding( - get: { editingText }, - set: { _ in } // Handle in the callbacks - ) - - let swiftUIView = AnyView( - EditableScriptLineView( - line: line, - isEditing: isEditing, - isSelected: isSelected, - isEditingText: isEditingText, - isSelectingForSection: isSelectingForSection, - editingText: editingTextBinding, - onToggleSelection: onToggleSelection, - onStartTextEdit: onStartTextEdit, - onFinishTextEdit: onFinishTextEdit, - onInsertAfter: onInsertAfter, - onEditFlags: onEditFlags - ) - .padding(.horizontal, 16) - .padding(.vertical, 4) - ) - - if let hostingController = hostingController { - hostingController.rootView = swiftUIView - } else { - hostingController = UIHostingController(rootView: swiftUIView) - hostingController!.view.backgroundColor = .clear - hostingController!.view.translatesAutoresizingMaskIntoConstraints = false - - contentView.addSubview(hostingController!.view) - NSLayoutConstraint.activate([ - hostingController!.view.topAnchor.constraint(equalTo: contentView.topAnchor), - hostingController!.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - hostingController!.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - hostingController!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - - // Ensure proper selection state - selectionStyle = .none - backgroundColor = .clear - } -} - -// UITableViewCell that hosts SwiftUI section content -class HostingSectionCell: UITableViewCell { - private var hostingController: UIHostingController? - - override func prepareForReuse() { - super.prepareForReuse() - // Clean up when cell is reused - hostingController?.view.removeFromSuperview() - hostingController = nil - } - - func configure(with section: ScriptSection, onTap: @escaping (ScriptSection) -> Void) { - let swiftUIView = AnyView( - Button(action: { onTap(section) }) { - SectionHeaderView(section: section) - } - .buttonStyle(PlainButtonStyle()) - .padding(.horizontal, 16) - .padding(.top, 8) - ) - - if let hostingController = hostingController { - hostingController.rootView = swiftUIView - } else { - hostingController = UIHostingController(rootView: swiftUIView) - hostingController!.view.backgroundColor = .clear - hostingController!.view.translatesAutoresizingMaskIntoConstraints = false - - contentView.addSubview(hostingController!.view) - NSLayoutConstraint.activate([ - hostingController!.view.topAnchor.constraint(equalTo: contentView.topAnchor), - hostingController!.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - hostingController!.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - hostingController!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - - // Ensure proper selection state - selectionStyle = .none - backgroundColor = .clear - } -} - - -class HostingReadingLineCell: UITableViewCell { - private var hostingController: UIHostingController? - - override func prepareForReuse() { - super.prepareForReuse() - hostingController?.view.removeFromSuperview() - hostingController = nil - } - - func configure( - line: ScriptLine, - isSelected: Bool, - isEditing: Bool, - editingText: String, - onElementTap: @escaping (LineElement) -> Void, - onLineTap: @escaping () -> Void, - onEditComplete: @escaping (String) -> Void, - onCueDelete: @escaping (Cue) -> Void, - onCueEdit: @escaping (Cue) -> Void - ) { - let editingTextBinding = Binding( - get: { editingText }, - set: { _ in } - ) - - let swiftUIView = AnyView( - ScriptLineView( - line: line, - isSelected: isSelected, - isEditing: isEditing, - editingText: editingTextBinding, - onElementTap: onElementTap, - onLineTap: onLineTap, - onEditComplete: onEditComplete, - onCueDelete: onCueDelete, - onCueEdit: onCueEdit - ) - .padding(.horizontal, 16) - .padding(.vertical, 4) - ) - - if let hostingController = hostingController { - hostingController.rootView = swiftUIView - } else { - hostingController = UIHostingController(rootView: swiftUIView) - hostingController!.view.backgroundColor = .clear - hostingController!.view.translatesAutoresizingMaskIntoConstraints = false - - contentView.addSubview(hostingController!.view) - NSLayoutConstraint.activate([ - hostingController!.view.topAnchor.constraint(equalTo: contentView.topAnchor), - hostingController!.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - hostingController!.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - hostingController!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) - } - - selectionStyle = .none - backgroundColor = .clear - } -}