#!/bin/bash # 1. Create Project Directories echo "Creating folder structure..." mkdir -p TagMaster/Sources/App mkdir -p TagMaster/Sources/Views mkdir -p TagMaster/Sources/Models mkdir -p TagMaster/Sources/ViewModels mkdir -p TagMaster/Sources/Services cd TagMaster # 2. Create project.yml for XcodeGen echo "Generating project.yml..." cat << 'EOF' > project.yml name: TagMaster options: bundleIdPrefix: com.tagmaster deploymentTarget: iOS: "17.0" targets: TagMaster: type: application platform: iOS sources: [Sources] info: path: Info.plist properties: UILaunchScreen: {} UIFileSharingEnabled: true LSSupportsOpeningDocumentsInPlace: true dependencies: - package: ZIPFoundation packages: ZIPFoundation: url: https://github.com/weichsel/ZIPFoundation.git from: 0.9.19 EOF # 3. Create Info.plist stub (XcodeGen can auto-gen most, but we need minimal) cat << 'EOF' > Info.plist EOF # 4. Create App Entry Point echo "Writing App.swift..." cat << 'EOF' > Sources/App/TagMasterApp.swift import SwiftUI @main struct TagMasterApp: App { var body: some Scene { WindowGroup { MainImportView() } } } EOF # 5. Create the Models echo "Writing Models..." cat << 'EOF' > Sources/Models/Track.swift import Foundation import SwiftUI struct Track: Identifiable, Hashable { let id = UUID() var originalURL: URL? var songName: String = "" var trackNumber: String = "" var discNumber: String = "" } struct BatchMetadata { var artist: String = "" var album: String = "" var albumArtist: String = "" var composer: String = "" var grouping: String = "" var genre: String = "Speech" var year: String = "" var compilation: Bool = false var rating: Int = 0 var bpm: String = "" var comments: String = "" var coverArt: UIImage? = nil } EOF # 6. Create the ViewModel echo "Writing ViewModel..." cat << 'EOF' > Sources/ViewModels/EditorViewModel.swift import SwiftUI import UniformTypeIdentifiers @MainActor class EditorViewModel: ObservableObject { @Published var batchData = BatchMetadata() @Published var tracks: [Track] = [] @Published var isProcessing = false @Published var shareZipURL: URL? // Simulating the import process func importFiles() { // In reality, you'd use a UIDocumentPickerViewController or ZIPFoundation to extract tracks = [ Track(songName: "Preface", trackNumber: "1", discNumber: "1"), Track(songName: "Chapter 1", trackNumber: "2", discNumber: "1") ] } func exportAndZip() async { isProcessing = true // 1. Create staging directory let fm = FileManager.default let tempDir = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString) try? fm.createDirectory(at: tempDir, withIntermediateDirectories: true) // 2. Save Cover Art if let cover = batchData.coverArt, let data = cover.jpegData(compressionQuality: 0.8) { let coverURL = tempDir.appendingPathComponent("cover.jpg") try? data.write(to: coverURL) } // 3. Process, Rename, and Move Files // Format: [Disc]-[Track]-[Songname].[ext] for track in tracks { let safeSong = track.songName.replacingOccurrences(of: "/", with: "-") let dNum = track.discNumber.isEmpty ? "01" : String(format: "%02d", Int(track.discNumber) ?? 1) let tNum = track.trackNumber.isEmpty ? "01" : String(format: "%02d", Int(track.trackNumber) ?? 1) let filename = "\(dNum)-\(tNum)-\(safeSong).flac" // Defaulting to flac for demo let destinationURL = tempDir.appendingPathComponent(filename) // Here you would write the ID3 metadata to the file using AVAssetExportSession or ID3TagEditor // and copy it to the destinationURL. try? "Simulated Audio Data".write(to: destinationURL, atomically: true, encoding: .utf8) } // 4. Zip the folder let zipURL = fm.temporaryDirectory.appendingPathComponent("\(batchData.album.isEmpty ? "Export" : batchData.album).zip") // NOTE: Use ZIPFoundation here to zip `tempDir` to `zipURL` // try? fm.zipItem(at: tempDir, to: zipURL) // 5. Present Share Sheet self.shareZipURL = zipURL self.isProcessing = false } } EOF # 7. Create Views echo "Writing Views..." cat << 'EOF' > Sources/Views/MainImportView.swift import SwiftUI struct MainImportView: View { @StateObject private var viewModel = EditorViewModel() @State private var showEditor = false var body: some View { NavigationStack { VStack { Button(action: { viewModel.importFiles() showEditor = true }) { VStack(spacing: 20) { Image(systemName: "plus.circle.fill") .font(.system(size: 80)) Text("Import Zip or Audio Files") .font(.title2.bold()) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(uiColor: .secondarySystemBackground)) .cornerRadius(20) .padding() } .buttonStyle(PlainButtonStyle()) } .navigationTitle("TagMaster") .navigationDestination(isPresented: $showEditor) { EditorView(viewModel: viewModel) } } } } EOF cat << 'EOF' > Sources/Views/EditorView.swift import SwiftUI struct EditorView: View { @ObservedObject var viewModel: EditorViewModel @State private var showingImagePicker = false var body: some View { Form { Section(header: Text("Album Artwork")) { HStack { Spacer() Button(action: { showingImagePicker = true }) { if let img = viewModel.batchData.coverArt { Image(uiImage: img) .resizable() .scaledToFit() .frame(width: 150, height: 150) .cornerRadius(8) } else { ZStack { RoundedRectangle(cornerRadius: 8) .fill(Color(uiColor: .systemGray5)) .frame(width: 150, height: 150) Image(systemName: "photo") .font(.largeTitle) .foregroundColor(.gray) } } } Spacer() } } Section(header: Text("Batch Metadata (Applies to all)")) { TextField("Artist", text: $viewModel.batchData.artist) TextField("Album", text: $viewModel.batchData.album) TextField("Album Artist", text: $viewModel.batchData.albumArtist) TextField("Composer", text: $viewModel.batchData.composer) TextField("Grouping", text: $viewModel.batchData.grouping) TextField("Genre", text: $viewModel.batchData.genre) TextField("Year", text: $viewModel.batchData.year) .keyboardType(.numberPad) Toggle("Compilation (Various Artists)", isOn: $viewModel.batchData.compilation) TextField("BPM", text: $viewModel.batchData.bpm) .keyboardType(.numberPad) TextField("Comments", text: $viewModel.batchData.comments) } Section(header: Text("Individual Track Data")) { ForEach($viewModel.tracks) { $track in VStack(alignment: .leading) { TextField("Song Name", text: $track.songName) .font(.headline) HStack { TextField("Disc", text: $track.discNumber) .keyboardType(.numberPad) .frame(width: 50) Text("-") TextField("Track", text: $track.trackNumber) .keyboardType(.numberPad) .frame(width: 50) } } .padding(.vertical, 4) } } } .navigationTitle("Edit Tags") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Export ZIP") { Task { await viewModel.exportAndZip() } } .disabled(viewModel.isProcessing) } } .sheet(item: Binding( get: { viewModel.shareZipURL.map { ShareURL(url: $0) } }, set: { if $0 == nil { viewModel.shareZipURL = nil } } )) { shareItem in ShareSheet(activityItems: [shareItem.url]) } } } // Helpers for ShareSheet mapping struct ShareURL: Identifiable { let id = UUID() let url: URL } struct ShareSheet: UIViewControllerRepresentable { var activityItems: [Any] func makeUIViewController(context: Context) -> UIActivityViewController { UIActivityViewController(activityItems: activityItems, applicationActivities: nil) } func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} } EOF echo "Done! Run 'xcodegen' in the TagMaster directory to generate your .xcodeproj."