diff --git a/Promptly.xcodeproj/project.pbxproj b/Promptly.xcodeproj/project.pbxproj index 1fd713a..5ab0761 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 = 20; + CURRENT_PROJECT_VERSION = 5; 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.4; + MARKETING_VERSION = 1.0.5; 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 = 20; + CURRENT_PROJECT_VERSION = 5; 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.4; + MARKETING_VERSION = 1.0.5; 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 48a8dec..ca8c7b9 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.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist b/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist index ac39bd4..4130238 100644 --- a/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ Promptly-WatchOS Watch App.xcscheme_^#shared#^_ orderHint - 1 + 0 Promptly.xcscheme_^#shared#^_ orderHint - 0 + 1 diff --git a/Promptly/PromptlyApp.swift b/Promptly/PromptlyApp.swift index 4795a30..42ff366 100644 --- a/Promptly/PromptlyApp.swift +++ b/Promptly/PromptlyApp.swift @@ -53,25 +53,9 @@ extension PromptlyApp: WhatsNewCollectionProvider { /// A WhatsNewCollection var whatsNewCollection: WhatsNewCollection { WhatsNew( - version: "1.0.4", + version: "1.0.5", title: "DSMPrompt", features: [ - .init( - image: .init( - systemName: "square.and.arrow.down.on.square", - foregroundColor: .orange - ), - title: "Import & Export Show Files", - subtitle: "Import and export show files to have manual backups / share between members." - ), - .init( - image: .init( - systemName: "wand.and.stars", - foregroundColor: .cyan - ), - title: "What's New View", - subtitle: "Find out what's changed between versions." - ), .init( image: .init( systemName: "hammer", @@ -79,14 +63,6 @@ extension PromptlyApp: WhatsNewCollectionProvider { ), title: "Bug Fixes", subtitle: "Bug fixes and stability improvements." - ), - .init( - image: .init( - systemName: "hammer", - foregroundColor: .red - ), - title: "PDF Rendering", - subtitle: "Fixed bug where if you were in dark mode the text would not show in PDF exports." ) ], primaryAction: .init( diff --git a/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift b/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift index e93eaff..522af2d 100644 --- a/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift +++ b/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift @@ -48,6 +48,9 @@ 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 } } @@ -126,7 +129,7 @@ struct EditScriptView: View { } } .sheet(isPresented: $showingFlagEditor) { - if selectedLines.count == 1, let line = lineBeingFlagged { + if selectedLines.count == 1, let line = selectedLinesArray.first { FlagEditorView(line: line) { try? modelContext.save() refreshTrigger.refresh() @@ -195,6 +198,9 @@ struct EditScriptView: View { } .navigationTitle(Text(script.name)) .navigationBarTitleDisplayMode(.inline) + .task { + lineGroups = await groupLinesBySection(lines: sortedLines, sections: sortedSections) + } } // MARK: - View Components @@ -287,95 +293,127 @@ struct EditScriptView: View { } private var scriptContentView: some View { - ScrollView { - LazyVStack(spacing: 8) { - ForEach(groupLinesBySection(), id: \.id) { group in - if let section = group.section { - SectionHeaderView(section: section) - .padding(.horizontal) - .padding(.top, 8) - } - - ForEach(group.lines, 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 - } - } + 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 + // ) } - .padding() - } + ) } // MARK: - Actions - private func groupLinesBySection() -> [LineGroup] { - // For performance, only process visible sections - let maxVisibleSections = 20 - let limitedSections = Array(sortedSections.prefix(maxVisibleSections)) + 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 lastProcessedLine = 0 + var processedLines = Set() - for section in limitedSections { - let startLine = section.startLineNumber + for (index, startLine, endLine) in sectionRanges { + let section = sections[index] - // Find the next section's start line (or end of script) - let nextSectionStart = sortedSections.first { - $0.startLineNumber > startLine - }?.startLineNumber ?? (sortedLines.last?.lineNumber ?? 0) + 1 + let startIdx = binarySearchStart(lines: lines, lineNumber: startLine) + let endIdx = binarySearchEnd(lines: lines, lineNumber: endLine) - // Add ungrouped lines before this section - if startLine > lastProcessedLine + 1 { - let ungroupedLines = sortedLines.filter { - $0.lineNumber > lastProcessedLine && $0.lineNumber < startLine - } - if !ungroupedLines.isEmpty { - groups.append(LineGroup(section: nil, lines: ungroupedLines)) - } - } + guard startIdx < lines.count && endIdx >= 0 else { continue } - // Add this section's lines (only up to next section) - let sectionLines = sortedLines.filter { - $0.lineNumber >= startLine && $0.lineNumber < nextSectionStart - } + let sectionLines = lines[startIdx...min(endIdx, lines.count - 1)] + .filter { !processedLines.contains($0.lineNumber) } if !sectionLines.isEmpty { - groups.append(LineGroup(section: section, lines: sectionLines)) - lastProcessedLine = sectionLines.last?.lineNumber ?? lastProcessedLine + groups.append(LineGroup(section: section, lines: Array(sectionLines))) + processedLines.formUnion(sectionLines.map { $0.lineNumber }) } } - // Add remaining ungrouped lines - let remainingLines = sortedLines.filter { $0.lineNumber > lastProcessedLine } - if !remainingLines.isEmpty { - groups.append(LineGroup(section: nil, lines: remainingLines)) + let ungroupedLines = lines.filter { !processedLines.contains($0.lineNumber) } + if !ungroupedLines.isEmpty { + groups.append(LineGroup(section: nil, lines: ungroupedLines)) } - return groups + 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 } private func updateSectionsAfterLineChange(at changePoint: Int, delta: Int) { @@ -667,7 +705,7 @@ struct EditableScriptLineView: View { private var backgroundOpacity: Double { if isSelected && isEditing { - return 0.6 + return 1.0 } else if line.isMarked { return 0.3 } else { diff --git a/Promptly/Views/Scripts/ScriptEditorView.swift b/Promptly/Views/Scripts/ScriptEditorView.swift index 95a0f94..4e849f0 100644 --- a/Promptly/Views/Scripts/ScriptEditorView.swift +++ b/Promptly/Views/Scripts/ScriptEditorView.swift @@ -180,41 +180,26 @@ struct ScriptEditorView: View { } private var scriptContentView: some View { - ScrollViewReader { proxy in - ScrollView { - LazyVStack(alignment: .leading, spacing: 12) { - ForEach(lineGroups, id: \.id) { group in - if let section = group.section { - SectionHeaderView(section: section) - .padding(.horizontal, 16) - .padding(.top, 8) - } - - ForEach(group.lines, 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() + 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 + } - } + ) } private func handleElementTap(element: LineElement, line: ScriptLine) { diff --git a/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift b/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift new file mode 100644 index 0000000..b35a195 --- /dev/null +++ b/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift @@ -0,0 +1,416 @@ +// +// 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 + parent.onElementTap?(element, item.line!) + }, + onLineTap: { + parent.onLineTap?(item.line!) + }, + onEditComplete: { newText in + 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 + } +}