From cd4e3a0961becea5d35f2d45a2905fc2f16a10a9 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Wed, 8 Apr 2026 17:40:46 -0700 Subject: [PATCH] Instal app test --- Sources/Models/Track.swift | 47 ++++++- Sources/Views/EditorView.swift | 101 --------------- Sources/Views/MainImportView.swift | 189 ++++++++++++++++++++++++++--- 3 files changed, 214 insertions(+), 123 deletions(-) delete mode 100644 Sources/Views/EditorView.swift diff --git a/Sources/Models/Track.swift b/Sources/Models/Track.swift index 788e844..881249d 100644 --- a/Sources/Models/Track.swift +++ b/Sources/Models/Track.swift @@ -1,12 +1,14 @@ import Foundation import SwiftUI +import UniformTypeIdentifiers +// MARK: - Models struct Track: Identifiable, Hashable { let id = UUID() - var originalURL: URL? - var songName: String = "" - var trackNumber: String = "" - var discNumber: String = "" + var originalURL: URL + var songName: String + var trackNumber: String + var discNumber: String } struct BatchMetadata { @@ -15,7 +17,7 @@ struct BatchMetadata { var albumArtist: String = "" var composer: String = "" var grouping: String = "" - var genre: String = "Speech" + var genre: String = "Speech" // Defaulting to your UI reference var year: String = "" var compilation: Bool = false var rating: Int = 0 @@ -23,3 +25,38 @@ struct BatchMetadata { var comments: String = "" var coverArt: UIImage? = nil } + +// MARK: - Processor Protocol +protocol AudioTagProcessor { + var supportedExtensions: [String] { get } + func writeMetadata(_ metadata: BatchMetadata, trackData: Track, to fileURL: URL) async throws +} + +// MARK: - Factory & Dummy Implementations +class TagProcessorFactory { + static let processors: [AudioTagProcessor] = [ + MP3TagProcessor(), + FLACTagProcessor() + ] + + static func processorFor(extension ext: String) -> AudioTagProcessor? { + return processors.first(where: { $0.supportedExtensions.contains(ext.lowercased()) }) + } +} + +// You will bridge your specific tagging libraries inside these structs +struct MP3TagProcessor: AudioTagProcessor { + let supportedExtensions = ["mp3"] + func writeMetadata(_ metadata: BatchMetadata, trackData: Track, to fileURL: URL) async throws { + // Implement ID3TagEditor logic here + print("Writing MP3 tags to \(fileURL.lastPathComponent)") + } +} + +struct FLACTagProcessor: AudioTagProcessor { + let supportedExtensions = ["flac"] + func writeMetadata(_ metadata: BatchMetadata, trackData: Track, to fileURL: URL) async throws { + // Implement FLAC/Vorbis comment logic here + print("Writing FLAC tags to \(fileURL.lastPathComponent)") + } +} diff --git a/Sources/Views/EditorView.swift b/Sources/Views/EditorView.swift deleted file mode 100644 index 5259606..0000000 --- a/Sources/Views/EditorView.swift +++ /dev/null @@ -1,101 +0,0 @@ -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) {} -} diff --git a/Sources/Views/MainImportView.swift b/Sources/Views/MainImportView.swift index c189593..975e6e8 100644 --- a/Sources/Views/MainImportView.swift +++ b/Sources/Views/MainImportView.swift @@ -1,33 +1,188 @@ import SwiftUI +import UniformTypeIdentifiers +// MARK: - Main Drop Zone struct MainImportView: View { @StateObject private var viewModel = EditorViewModel() @State private var showEditor = false + @State private var isTargeted = false + @State private var showFilePicker = 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() + ZStack { + Color(uiColor: .systemGroupedBackground).edgesIgnoringSafeArea(.all) + + VStack(spacing: 20) { + Image(systemName: "folder.badge.plus") + .font(.system(size: 80)) + .foregroundColor(isTargeted ? .accentColor : .gray) + + Text("Drop a .zip file here") + .font(.title.bold()) + + Text("Or tap to browse files") + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + RoundedRectangle(cornerRadius: 24) + .strokeBorder(style: StrokeStyle(lineWidth: 4, dash: [10])) + .foregroundColor(isTargeted ? .accentColor : .gray.opacity(0.5)) + ) + .padding(40) + .onDrop(of: [.zip], isTargeted: $isTargeted) { providers in + if let provider = providers.first { + provider.loadItem(forTypeIdentifier: UTType.zip.identifier, options: nil) { urlData, error in + if let url = urlData as? URL { + DispatchQueue.main.async { + viewModel.processZip(at: url) + showEditor = true + } + } + } + return true + } + return false + } + .onTapGesture { + showFilePicker = true } - .buttonStyle(PlainButtonStyle()) } .navigationTitle("TagMaster") .navigationDestination(isPresented: $showEditor) { - EditorView(viewModel: viewModel) + EditorSplitView(viewModel: viewModel) + } + .fileImporter(isPresented: $showFilePicker, allowedContentTypes: [.zip]) { result in + switch result { + case .success(let url): + if url.startAccessingSecurityScopedResource() { + viewModel.processZip(at: url) + showEditor = true + url.stopAccessingSecurityScopedResource() + } + case .failure(let error): + print(error.localizedDescription) + } } } } } + +// MARK: - Split View Editor +struct EditorSplitView: View { + @ObservedObject var viewModel: EditorViewModel + + var body: some View { + NavigationSplitView { + // LEFT COLUMN: Batch Settings + Form { + Section { + VStack { + if let img = viewModel.batchData.coverArt { + Image(uiImage: img) + .resizable() + .scaledToFit() + .frame(height: 200) + .cornerRadius(12) + } else { + ZStack { + RoundedRectangle(cornerRadius: 12) + .fill(Color(uiColor: .secondarySystemFill)) + .frame(height: 200) + Image(systemName: "photo.badge.plus") + .font(.largeTitle) + .foregroundColor(.gray) + } + } + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } + + Section(header: Text("Batch Data")) { + 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("Genre", text: $viewModel.batchData.genre) + TextField("Year", text: $viewModel.batchData.year).keyboardType(.numberPad) + Toggle("Compilation", isOn: $viewModel.batchData.compilation) + } + } + .navigationTitle("Batch Settings") + .navigationBarTitleDisplayMode(.inline) + + } detail: { + // RIGHT COLUMN: Track List + ZStack { + List { + Section(header: Text("Tracks (\(viewModel.tracks.count))")) { + ForEach($viewModel.tracks) { $track in + HStack(spacing: 16) { + VStack { + TextField("D", text: $track.discNumber).frame(width: 30).textFieldStyle(.roundedBorder) + TextField("T", text: $track.trackNumber).frame(width: 30).textFieldStyle(.roundedBorder) + } + + TextField("Song Title", text: $track.songName) + .font(.headline) + .textFieldStyle(.roundedBorder) + } + .padding(.vertical, 4) + } + } + } + .navigationTitle("Tracklist") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Export to ZIP") { + Task { + await viewModel.exportAndZip() + } + } + .bold() + .disabled(viewModel.isProcessing || viewModel.tracks.isEmpty) + } + } + + // Processing Overlay + if viewModel.isProcessing { + Color.black.opacity(0.4).edgesIgnoringSafeArea(.all) + VStack(spacing: 16) { + ProgressView() + .scaleEffect(1.5) + .tint(.white) + Text(viewModel.processingStatus) + .foregroundColor(.white) + .font(.headline) + } + .padding(30) + .background(.ultraThinMaterial) + .cornerRadius(16) + } + } + } + .sheet(item: Binding( + get: { viewModel.shareZipURL.map { ShareURL(url: $0) } }, + set: { if $0 == nil { viewModel.shareZipURL = nil } } + )) { shareItem in + ShareSheet(activityItems: [shareItem.url]) + } + } +} + +// Helpers +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) {} +}