diff --git a/generate.sh b/generate.sh new file mode 100644 index 0000000..fe4d5ef --- /dev/null +++ b/generate.sh @@ -0,0 +1,299 @@ +#!/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."