//
// CharacterAudioSettingsView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
import AVFoundation
struct CharacterAudioSettingsView: View {
@Environment(\.presentationMode) var presentationMode
@Binding var play: Play
@State private var selectedVoice: [AVSpeechSynthesisVoice] = []
@State private var speechRate: [Float] = []
var body: some View {
NavigationView {
List {
ForEach(Array(play.characters.enumerated()), id: \.element.id) { index, character in
VStack(alignment: .leading, spacing: 10) {
Text(character.name)
.font(.headline)
HStack {
Text("Voice:")
Picker("Voice", selection: $selectedVoice[index]) {
ForEach(AVSpeechSynthesisVoice.speechVoices(), id: \.self) { voice in
Text(voice.name).tag(voice)
}
}
.pickerStyle(MenuPickerStyle())
}
HStack {
Text("Speech Rate:")
Slider(value: $speechRate[index], in: AVSpeechUtteranceMinimumSpeechRate...AVSpeechUtteranceMaximumSpeechRate)
}
}
}
}
.navigationTitle("Character Audio Settings")
.navigationBarItems(trailing: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
})
.onAppear {
initializeSettings()
}
}
}
func initializeSettings() {
for _ in play.characters {
selectedVoice.append(AVSpeechSynthesisVoice(language: "en-US")!)
speechRate.append(AVSpeechUtteranceDefaultSpeechRate)
}
}
}
//
// CharacterImportView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
struct CharacterImportView: View {
@Environment(\.presentationMode) var presentationMode
@Binding var characters: [TheaterCharacter]
@State private var characterNames: [String] = []
@State private var importMethod = ImportMethod.camera
@State private var showAlert = false
@State private var alertTitle = ""
@State private var alertMessage = ""
var body: some View {
NavigationView {
VStack {
Form {
Picker("Import Method", selection: $importMethod) {
Text("Camera").tag(ImportMethod.camera)
Text("Photo Library").tag(ImportMethod.photoLibrary)
Text("PDF").tag(ImportMethod.pdf)
}
}
Button(action: {
importCharacters()
}) {
Text("Import Characters")
}
.padding()
}
.navigationTitle("Import Characters")
.navigationBarItems(trailing: Button("Done") {
presentationMode.wrappedValue.dismiss()
})
.alert(isPresented: $showAlert) {
Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
}
}
}
private func importCharacters() {
switch importMethod {
case .camera:
// Camera import logic will be added here.
break
case .photoLibrary:
// Photo library import logic will be added here.
break
case .pdf:
// PDF import logic will be added here.
break
}
}
private func processCharacterList(_ characterList: String) {
let lines = characterList.split(separator: "\n").map(String.init)
for line in lines {
let name = line.trimmingCharacters(in: .whitespacesAndNewlines)
if !name.isEmpty {
// Add default values for voiceLanguage and voiceRate
let character = TheaterCharacter(id: UUID(), name: name, voiceLanguage: "en-US", voiceRate: 0.5)
characters.append(character)
}
}
}
private enum ImportMethod: Int, CaseIterable, Identifiable {
case camera
case photoLibrary
case pdf
var id: Int { rawValue }
}
}
//
// CustomScriptImportView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
import PhotosUI
import Vision
import Foundation
class CustomScriptStorage: ObservableObject {
@Published var scripts: [Script] = []
}
struct CustomScriptImportView: View {
@EnvironmentObject var scriptStorage: CustomScriptStorage
@State private var showCharacterSelection = false
@State private var showImagePicker = false
@State private var script: Script = Script(id: UUID(), title: "", characters: [], dialogues: [])
@State private var showAlert = false
@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var isProcessing = false
@State private var selectedImage: UIImage?
var body: some View {
NavigationView {
VStack {
Text("Import Your Script")
.font(.title)
.padding()
Button(action: {
showImagePicker = true
}) {
Text("Import from Photo Library")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.sheet(isPresented: $showImagePicker) {
ImagePicker(selectedImage: $selectedImage)
}
.onChange(of: selectedImage) { newImage in
guard let image = newImage else { return }
isProcessing = true
performOCR(on: image) { recognizedText in
isProcessing = false
guard let recognizedText = recognizedText else {
showAlert(title: "Error", message: "Failed to recognize text")
return
}
processScript(recognizedText)
}
}
}
.padding()
.navigationBarTitle("Script Import", displayMode: .inline)
.alert(isPresented: $showAlert) {
Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
}
.overlay(isProcessing ? ProgressView("Processing...") : nil)
}
}
private func showAlert(title: String, message: String) {
alertTitle = title
alertMessage = message
showAlert = true
}
private func performOCR(on image: UIImage, completion: @escaping (String?) -> Void) {
guard let cgImage = image.cgImage else {
completion(nil)
return
}
let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
let request = VNRecognizeTextRequest { request, error in
if let error = error {
print("Error recognizing text: \(error)")
completion(nil)
return
}
guard let observations = request.results as? [VNRecognizedTextObservation] else {
completion(nil)
return
}
let recognizedText = observations.compactMap { observation in
observation.topCandidates(1).first?.string
}.joined(separator: "\n")
completion(recognizedText)
}
request.recognitionLevel = .accurate
do {
try requestHandler.perform([request])
} catch {
print("Error performing OCR: \(error)")
completion(nil)
}
}
private func processScript(_ recognizedText: String) {
let lines = recognizedText.split(separator: "\n")
var currentCharacter: TheaterCharacter?
var dialogues: [Dialogue] = []
for line in lines {
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedLine.isEmpty { continue }
let newCharacter = TheaterCharacter(id: UUID(), name: trimmedLine, voiceLanguage: "en-US", voiceRate: 0.5)
if let existingCharacter = script.characters.first(where: { $0.name == newCharacter.name }) {
currentCharacter = existingCharacter
} else if let currentCharacter = currentCharacter {
dialogues.append(Dialogue(id: UUID(), characterID: currentCharacter.id, content: String(line)))
}
}
let script = Script(id: UUID(), title: "Untitled", characters: [], dialogues: dialogues)
scriptStorage.scripts.append(script)
isProcessing = false
showAlert(title: "Success", message: "Script successfully imported!")
}
}
struct CustomScriptImportView_Previews: PreviewProvider {
static var previews: some View {
CustomScriptImportView().environmentObject(CustomScriptStorage())
}
}
//
// DialogueImportView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
struct DialogueImportView: View {
@Environment(\.presentationMode) private var presentationMode
@ObservedObject var play: Play
@State private var importMethod: ImportMethod = .camera
@State private var dialogues: [Dialogue] = []
var body: some View {
VStack {
Text("Import Dialogue")
.font(.largeTitle)
.padding()
Picker("Import Method", selection: $importMethod) {
ForEach(ImportMethod.allCases) { method in
Text(method.description).tag(method)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Button(action: importDialogues) {
Text("Import")
.font(.title)
.padding()
}
.disabled(dialogues.isEmpty)
Spacer()
}
}
}
extension DialogueImportView {
enum ImportMethod: String, CaseIterable, Identifiable {
case camera
case photoLibrary
case pdf
var id: String { rawValue }
var description: String {
switch self {
case .camera:
return "Camera"
case .photoLibrary:
return "Photo Library"
case .pdf:
return "PDF"
}
}
}
private func importDialogues() {
switch importMethod {
case .camera:
// Add camera functionality for importing dialogues
break
case .photoLibrary:
// Add photo library functionality for importing dialogues
break
case .pdf:
// Add PDF importing functionality for importing dialogues
break
}
}
}
struct DialogueImportView_Previews: PreviewProvider {
static var previews: some View {
DialogueImportView(play: Play.example)
}
}
//
// ImagePicker.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 4/4/23.
//
import SwiftUI
import PhotosUI
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let controller = PHPickerViewController(configuration: config)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
guard let result = results.first else {
return
}
let itemProvider = result.itemProvider
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
DispatchQueue.main.async {
if let image = image as? UIImage {
self?.parent.selectedImage = image
} else {
print("Failed to load image:", error?.localizedDescription ?? "unknown error")
}
}
}
}
}
}
}
//
// NewPlayView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
struct NewPlayView: View {
@EnvironmentObject var playStorage: PlayStorage
@Environment(\.presentationMode) var presentationMode
@State private var playTitle: String = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter play title", text: $playTitle)
.padding()
Button(action: {
let newPlay = Play(id: UUID(), title: playTitle, acts: [], characters: [])
playStorage.plays.append(newPlay)
presentationMode.wrappedValue.dismiss()
}) {
Text("Add Play")
.font(.title2)
.padding()
}
}
.padding()
.navigationBarTitle("New Play", displayMode: .inline)
}
}
}
//
// Persistence.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/22/23.
//
import CoreData
class PersistenceController: ObservableObject {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "ThatsYourCue")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
//
// Play.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import Foundation
class Play: Identifiable, Codable, ObservableObject {
let id: UUID
let title: String
let acts: [Act]
let characters: [TheaterCharacter]
init(id: UUID, title: String, acts: [Act], characters: [TheaterCharacter]) {
self.id = id
self.title = title
self.acts = acts
self.characters = characters
}
// Add this function inside the Play class
func character(withId id: UUID) -> TheaterCharacter? {
return self.characters.first { $0.id == id }
}
// Add this function inside the Play class
func allCharacterLines() -> [(UUID, String)] {
var lines: [(UUID, String)] = []
for act in self.acts {
for scene in act.scenes {
for dialogue in scene.dialogues {
lines.append((dialogue.characterID, dialogue.content))
}
}
}
return lines
}
}
struct Act: Identifiable, Codable {
let id: UUID
let number: Int
let scenes: [Scene]
}
struct Scene: Identifiable, Codable {
let id: UUID
let number: Int
let dialogues: [Dialogue]
}
struct Dialogue: Identifiable, Codable {
let id: UUID
let characterID: UUID
let content: String
}
extension Play {
static func exampleData() -> [Play] {
let exampleCharacters = TheaterCharacter.exampleData()
return [
Play(id: UUID(), title: "Play 1", acts: [], characters: exampleCharacters),
Play(id: UUID(), title: "Play 2", acts: [], characters: exampleCharacters),
Play(id: UUID(), title: "Play 3", acts: [], characters: exampleCharacters)
]
}
static let example = Play(id: UUID(), title: "Example Play", acts: [], characters: TheaterCharacter.exampleData())
}
//
// PlayCreationView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
struct PlayCreationView: View {
@EnvironmentObject var playStorage: PlayStorage
@Environment(\.presentationMode) var presentationMode
@State private var playTitle: String
@State private var characters: [TheaterCharacter]
@State private var isImportingCharacters = false
init(play: Play? = nil) {
_playTitle = State(initialValue: play?.title ?? "")
_characters = State(initialValue: play?.characters ?? [])
}
func savePlay() {
let newPlay = Play(id: UUID(), title: playTitle, acts: [], characters: characters)
playStorage.plays.append(newPlay)
presentationMode.wrappedValue.dismiss()
}
var body: some View {
NavigationView {
VStack {
Form {
TextField("Enter play title", text: $playTitle)
Section(header: Text("Characters")) {
ForEach(characters) { character in
Text(character.name)
}
Button(action: {
isImportingCharacters.toggle()
}) {
HStack {
Image(systemName: "plus")
Text("Import Characters")
}
}
}
}
Spacer()
}
.navigationBarTitle("New Play", displayMode: .inline)
.navigationBarItems(trailing: Button("Save", action: savePlay))
.sheet(isPresented: $isImportingCharacters) {
CharacterImportView(characters: $characters)
}
}
}
}
struct PlayCreationView_Previews: PreviewProvider {
static var previews: some View {
PlayCreationView().environmentObject(PlayStorage())
}
}
//
// PlayDetailsView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
struct PlayDetailsView: View {
@EnvironmentObject var playStorage: PlayStorage
@ObservedObject var play: Play
@State private var showEditPlayView = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text(play.title)
.font(.largeTitle)
.fontWeight(.bold)
VStack(alignment: .leading, spacing: 10) {
Text("Characters")
.font(.title2)
.fontWeight(.bold)
ForEach(play.characters) { character in
Text(character.name)
.font(.body)
}
}
// Display acts, scenes, and dialogues here
}
.padding()
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showEditPlayView = true
}) {
Image(systemName: "pencil")
}
}
}
.sheet(isPresented: $showEditPlayView) {
PlayCreationView(play: play)
.environmentObject(playStorage)
}
}
}
struct PlayDetailsView_Previews: PreviewProvider {
static var previews: some View {
PlayDetailsView(play: Play.exampleData().first!)
.environmentObject(PlayStorage())
}
}
//
// PlayListView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
struct PlayListView: View {
@EnvironmentObject var playStorage: PlayStorage
@State private var isAddingNewPlay = false
var body: some View {
NavigationView {
List {
ForEach(playStorage.plays) { play in
NavigationLink(destination: PlayDetailsView(play: play)) {
Text(play.title)
}
}
.onDelete(perform: deletePlay)
}
.navigationBarTitle("Plays")
.navigationBarItems(
leading: EditButton(),
trailing: Button(action: {
isAddingNewPlay.toggle()
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $isAddingNewPlay) {
NewPlayView().environmentObject(playStorage)
}
}
}
private func deletePlay(at offsets: IndexSet) {
playStorage.plays.remove(atOffsets: offsets)
}
}
//
// PlayStorage.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
import Combine
class PlayStorage: ObservableObject {
@Published var plays: [Play] = []
private let saveKey = "SavedPlays"
private var autosave: AnyCancellable?
init() {
loadPlays()
autosave = $plays.sink { plays in
let encoder = JSONEncoder()
if let encodedPlays = try? encoder.encode(plays) {
UserDefaults.standard.set(encodedPlays, forKey: self.saveKey)
}
}
}
private func loadPlays() {
if let savedData = UserDefaults.standard.data(forKey: saveKey) {
let decoder = JSONDecoder()
if let decodedPlays = try? decoder.decode([Play].self, from: savedData) {
self.plays = decodedPlays
return
}
}
self.plays = []
}
}
//
// RecordingView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
import AVFoundation
struct RecordingView: View {
@Environment(\.presentationMode) var presentationMode
@Binding var play: Play
@State private var isRecording = false
@State private var audioRecorder: AVAudioRecorder?
@State private var characterLines: [(UUID, String)] = []
private var playValue: Play {
get { play }
set { play = newValue }
}
var body: some View {
VStack {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
ForEach(characterLines, id: \.0) { characterID, line in
if let character = playValue.character(withId: characterID) {
Text("\(character.name): \(line)")
.font(.body)
}
}
}
}
.padding(.horizontal)
HStack {
Button(action: {
if isRecording {
stopRecording()
} else {
startRecording()
}
}) {
Text(isRecording ? "Stop Recording" : "Start Recording")
.font(.title2)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(isRecording ? Color.red : Color.blue)
.cornerRadius(10)
.padding(.horizontal)
}
}
}
.navigationBarTitle("Recording", displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
})
.onAppear {
prepareCharacterLines()
setupAudioRecorder()
}
}
private func prepareCharacterLines() {
characterLines = playValue.allCharacterLines()
}
private func setupAudioRecorder() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playAndRecord, mode: .default)
try audioSession.setActive(true)
let recordingSettings = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
audioRecorder = try AVAudioRecorder(url: getDocumentsDirectory().appendingPathComponent("recording.m4a"), settings: recordingSettings)
audioRecorder?.prepareToRecord()
} catch {
print("Failed to set up audio recorder: \(error.localizedDescription)")
}
}
private func startRecording() {
isRecording = true
audioRecorder?.record()
}
private func stopRecording() {
isRecording = false
audioRecorder?.stop()
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
struct RecordingView_Previews: PreviewProvider {
static var previews: some View {
RecordingView(play: .constant(Play.example))
}
}
//
// RehearsalView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
import AVFoundation
struct CharacterLine {
let character: TheaterCharacter
let content: String
}
struct RehearsalView: View {
@Binding var play: Play
@State private var isPlaying = false
@State private var characterLines: [CharacterLine] = []
private var speechSynthesizer = AVSpeechSynthesizer()
private let speechDelegate = SpeechDelegate()
var body: some View {
VStack {
Text("Rehearsal")
.font(.largeTitle)
.padding()
if isPlaying {
Button(action: {
stopRehearsal()
}) {
Text("Stop Rehearsal")
.font(.title)
.padding()
}
} else {
Button(action: {
startRehearsal()
}) {
Text("Start Rehearsal")
.font(.title)
.padding()
}
}
}
.onAppear {
prepareCharacterLines()
speechSynthesizer.delegate = speechDelegate
}
}
// MARK: - Rehearsal Functions
func prepareCharacterLines() {
characterLines = []
for act in play.acts {
for scene in act.scenes {
for dialogue in scene.dialogues {
if let character = play.character(withId: dialogue.characterID) {
characterLines.append(CharacterLine(character: character, content: dialogue.content))
}
}
}
}
}
func startRehearsal() {
isPlaying = true
playCharacterLines(characterLines)
}
func stopRehearsal() {
isPlaying = false
speechSynthesizer.stopSpeaking(at: .immediate)
}
func playCharacterLines(_ lines: [CharacterLine]) {
guard !lines.isEmpty else {
isPlaying = false
return
}
let characterLine = lines[0]
let utterance = AVSpeechUtterance(speechString: characterLine.content)
utterance.voice = AVSpeechSynthesisVoice(language: characterLine.character.voiceLanguage)
utterance.rate = characterLine.character.voiceRate
speechSynthesizer.speak(utterance)
let estimatedSpeechDuration = utterance.estimatedSpeechDuration
DispatchQueue.main.asyncAfter(deadline: .now() + estimatedSpeechDuration) {
playCharacterLines(Array(lines.dropFirst()))
}
}
}
class SpeechDelegate: NSObject, AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
// Add any required functionality when speech finishes
}
}
extension AVSpeechUtterance {
private struct AssociatedKeys {
static var speechString = "speechString"
}
convenience init(speechString: String) {
self.init(string: speechString)
objc_setAssociatedObject(self, &AssociatedKeys.speechString, speechString, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
var speechString: String {
return objc_getAssociatedObject(self, &AssociatedKeys.speechString) as? String ?? ""
}
var estimatedSpeechDuration: TimeInterval {
let wordsPerMinute = 160.0
let words = self.speechString.split(separator: " ").count
let minutes = Double(words) / wordsPerMinute
return minutes * 60.0
}
}
//
// Script.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import Foundation
struct Script: Identifiable {
let id: UUID
let title: String
let characters: [TheaterCharacter]
let dialogues: [Dialogue]
}
//
// ScriptImportView.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import SwiftUI
import PhotosUI
import Vision
import Foundation
class CustomScriptStorage: ObservableObject {
@Published var scripts: [Script] = []
}
struct CustomScriptImportView: View {
@EnvironmentObject var scriptStorage: CustomScriptStorage
@State private var showCharacterSelection = false
@State private var showImagePicker = false
@State private var script: Script = Script(id: UUID(), title: "", characters: [], dialogues: [])
@State private var showAlert = false
@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var isProcessing = false
@State private var selectedImage: UIImage?
var body: some View {
NavigationView {
VStack {
Text("Import Your Script")
.font(.title)
.padding()
Button(action: {
showImagePicker = true
}) {
Text("Import from Photo Library")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.sheet(isPresented: $showImagePicker) {
ImagePicker(sourceType: .photoLibrary, selectedImage: $selectedImage)
}
.onChange(of: selectedImage) { newImage in
guard let image = newImage else { return }
isProcessing = true
performOCR(on: image) { recognizedText in
isProcessing = false
guard let recognizedText = recognizedText else {
showAlert(title: "Error", message: "Failed to recognize text")
return
}
processScript(recognizedText)
}
}
}
.padding()
.navigationBarTitle("Script Import", displayMode: .inline)
.alert(isPresented: $showAlert) {
Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
}
.overlay(isProcessing ? ProgressView("Processing...") : nil)
}
}
private func characterFromName(_ name: String) -> Character? {
for character in Character.allCases {
if character.name == name {
return character
}
}
return nil
}
private func showAlert(title: String, message: String) {
alertTitle = title
alertMessage = message
showAlert = true
}
private func performOCR(on image: UIImage, completion: @escaping (String?) -> Void) {
guard let cgImage = image.cgImage else {
completion(nil)
return
}
let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
let request = VNRecognizeTextRequest { request, error in
if let error = error {
print("Error recognizing text: \(error)")
completion(nil)
return
}
guard let observations = request.results as? [VNRecognizedTextObservation] else {
completion(nil)
return
}
let recognizedText = observations.compactMap { observation in
observation.topCandidates(1).first?.string
}.joined(separator: "\n")
completion(recognizedText)
}
request.recognitionLevel = .accurate
do {
try requestHandler.perform([request])
} catch {
print("Error performing OCR: \(error)")
completion(nil)
}
}
private func processScript(_ recognizedText: String) {
let lines = recognizedText.split(separator: "\n")
var currentCharacter: Character?
var dialogues: [Dialogue] = []
for line in lines {
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedLine.isEmpty { continue }
if let character = characterFromName(trimmedLine) {
currentCharacter = character
} else if let currentCharacter = currentCharacter {
dialogues.append(Dialogue(id: UUID(), characterID: currentCharacter.id, content: String(line)))
}
}
let script = Script(id: UUID(), title: "Untitled", characters: [], dialogues: dialogues)
scriptStorage.scripts.append(script)
isProcessing = false
showAlert(title: "Success", message: "Script successfully imported!")
}
}
struct CustomScriptImportView_Previews: PreviewProvider {
static var previews: some View {
CustomScriptImportView().environmentObject(CustomScriptStorage())
}
}
//
// ThatsYourCueApp.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/22/23.
//
import SwiftUI
@main
struct ThatsYourCueApp: App {
var body: some Scene {
WindowGroup {
PlayListView()
}
}
}
//
// Character.swift
// ThatsYourCue
//
// Created by Daniel Shearon on 3/31/23.
//
import Foundation
struct TheaterCharacter: Identifiable, Codable {
let id: UUID
let name: String
let voiceLanguage: String
let voiceRate: Float
}
extension TheaterCharacter {
static func exampleData() -> [TheaterCharacter] {
return [
TheaterCharacter(id: UUID(), name: "Character 1", voiceLanguage: "en-US", voiceRate: 0.5),
TheaterCharacter(id: UUID(), name: "Character 2", voiceLanguage: "en-US", voiceRate: 0.5),
TheaterCharacter(id: UUID(), name: "Character 3", voiceLanguage: "en-US", voiceRate: 0.5)
]
}
}