Instal app test

This commit is contained in:
Dallas Groot 2026-04-08 17:40:46 -07:00
parent 522fcc8db0
commit cd4e3a0961
3 changed files with 214 additions and 123 deletions

View file

@ -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)")
}
}

View file

@ -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) {}
}

View file

@ -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) {}
}