bitrig
EARLY ACCESS

Shared Project

Open in bitrigDownload Code
import SwiftUI
import AVFoundation

@main
struct ChapterMarkerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @State private var isRecording = false
    @State private var startTime = Date()
    @State private var elapsedTime: TimeInterval = 0
    @State private var timer: Timer?
    @State private var markers: [(time: TimeInterval, name: String)] = []
    @State private var audioEngine = AVAudioEngine()
    @State private var showingExport = false
    @State private var exportText = ""
    
    var formattedTime: String {
        let hours = Int(elapsedTime) / 3600
        let minutes = (Int(elapsedTime) % 3600) / 60
        let seconds = Int(elapsedTime) % 60
        return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
    }
    
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 30) {
                    // Timer Display
                    VStack(spacing: 10) {
                        Text(formattedTime)
                            .font(.system(size: 48, weight: .bold, design: .monospaced))
                            .foregroundColor(isRecording ? .red : .primary)
                        
                        Text(isRecording ? "Recording" : "Ready")
                            .font(.headline)
                            .foregroundColor(.secondary)
                    }
                    .padding()
                    
                    // Control Buttons
                    VStack(spacing: 15) {
                        Button(action: toggleRecording) {
                            HStack {
                                Image(systemName: isRecording ? "stop.fill" : "record.circle")
                                    .font(.title2)
                                Text(isRecording ? "Stop Recording" : "Start Recording")
                                    .font(.headline)
                            }
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(isRecording ? Color.red : Color.blue)
                            .cornerRadius(12)
                        }
                        
                        Button(action: addMarker) {
                            HStack {
                                Image(systemName: "bookmark.fill")
                                    .font(.title2)
                                Text("Add Chapter Marker")
                                    .font(.headline)
                            }
                            .foregroundColor(isRecording ? .white : .secondary)
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(isRecording ? Color.green : Color.gray.opacity(0.3))
                            .cornerRadius(12)
                        }
                        .disabled(!isRecording)
                        
                        if !markers.isEmpty && !isRecording {
                            Button(action: exportMarkers) {
                                HStack {
                                    Image(systemName: "square.and.arrow.up")
                                        .font(.title2)
                                    Text("Export Markers")
                                        .font(.headline)
                                }
                                .foregroundColor(.white)
                                .frame(maxWidth: .infinity)
                                .padding()
                                .background(Color.orange)
                                .cornerRadius(12)
                            }
                        }
                    }
                    .padding(.horizontal)
                    
                    // Markers List
                    if !markers.isEmpty {
                        VStack(alignment: .leading, spacing: 0) {
                            Text("Chapter Markers")
                                .font(.headline)
                                .padding(.horizontal)
                                .padding(.bottom, 10)
                            
                            LazyVStack(spacing: 1) {
                                ForEach(Array(markers.enumerated()), id: \.offset) { index, marker in
                                    HStack {
                                        Text(marker.name)
                                            .font(.body)
                                        
                                        Spacer()
                                        
                                        Text(timeString(from: marker.time))
                                            .font(.system(.body, design: .monospaced))
                                            .foregroundColor(.secondary)
                                    }
                                    .padding(.horizontal)
                                    .padding(.vertical, 12)
                                    .background(Color(.systemGray6))
                                }
                            }
                            .cornerRadius(10)
                        }
                        .padding(.horizontal)
                    }
                    
                    Spacer(minLength: 100)
                }
            }
            .navigationTitle("Chapter Markers")
            .sheet(isPresented: $showingExport) {
                NavigationStack {
                    ScrollView {
                        Text(exportText)
                            .font(.system(.body, design: .monospaced))
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .padding()
                    }
                    .navigationTitle("Export")
                    .navigationBarTitleDisplayMode(.inline)
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading) {
                            Button("Done") {
                                showingExport = false
                            }
                        }
                        ToolbarItem(placement: .navigationBarTrailing) {
                            ShareLink(item: exportText) {
                                Image(systemName: "square.and.arrow.up")
                            }
                        }
                    }
                }
            }
        }
        .onAppear {
            setupAudio()
        }
        .onReceive(Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()) { _ in
            if isRecording {
                elapsedTime = Date().timeIntervalSince(startTime)
            }
        }
    }
    
    private func setupAudio() {
        do {
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print("Audio setup failed")
        }
    }
    
    private func toggleRecording() {
        if isRecording {
            // Stop recording
            isRecording = false
            timer?.invalidate()
            timer = nil
        } else {
            // Start recording
            playStartSound()
            isRecording = true
            startTime = Date()
            elapsedTime = 0
            markers.removeAll()
        }
    }
    
    private func playStartSound() {
        guard let url = Bundle.main.url(forResource: "beep", withExtension: "wav") ?? 
              createBeepFile() else { return }
        
        do {
            let audioFile = try AVAudioFile(forReading: url)
            let audioPlayerNode = AVAudioPlayerNode()
            
            audioEngine.attach(audioPlayerNode)
            audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
            
            try audioEngine.start()
            
            audioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
            audioPlayerNode.play()
        } catch {
            // Fallback to system sound
            AudioServicesPlaySystemSound(1103)
        }
    }
    
    private func createBeepFile() -> URL? {
        // Create a simple beep tone programmatically if no audio file exists
        return nil
    }
    
    private func addMarker() {
        guard isRecording else { return }
        
        let markerNumber = markers.count + 1
        let markerName = "Marker \(markerNumber)"
        markers.append((time: elapsedTime, name: markerName))
    }
    
    private func exportMarkers() {
        var exportLines: [String] = []
        
        for marker in markers {
            let timeString = self.timeString(from: marker.time)
            exportLines.append("\(timeString) \(marker.name)")
        }
        
        exportText = exportLines.joined(separator: "\n")
        showingExport = true
    }
    
    private func timeString(from interval: TimeInterval) -> String {
        let hours = Int(interval) / 3600
        let minutes = (Int(interval) % 3600) / 60
        let seconds = Int(interval) % 60
        return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
    }
}