Instal app test
This commit is contained in:
parent
522fcc8db0
commit
cd4e3a0961
3 changed files with 214 additions and 123 deletions
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue