importer chnges

This commit is contained in:
Mark Gregory
2026-11-05 10:42:21 +00:00
parent faa1fe43d0
commit 1d6819d99e
3 changed files with 81 additions and 2 deletions
+69 -2
View File
@@ -651,6 +651,8 @@ struct LibraryView: View {
@State private var search = ""
@State private var name = ""
@State private var importing = false
@State private var importingServer = false
@State private var serverImportURLs = [URL]()
private var source: String { get { model.activeSection } nonmutating set { model.activeSection = newValue } }
@State private var singersExpanded = true
private var expandedSingers: Set<Int64> { get { model.expandedSingerIDs } nonmutating set { model.expandedSingerIDs = newValue } }
@@ -721,7 +723,10 @@ struct LibraryView: View {
.searchable(text:$search,placement:.toolbar,prompt:"Search Music")
.toolbar {
ToolbarItemGroup {
Button { importing = true } label: { Label("Import Music",systemImage:"square.and.arrow.down") }.help("Import music or video")
Menu {
Button("Import Files or Folder…") { importing = true }
Button("Import from SMB/NAS…") { importingServer = true }
} label: { Label("Import Music",systemImage:"square.and.arrow.down") }.help("Import from files, folders or a server drive")
Menu { waveformActions } label: { Label("Waveforms",systemImage:"waveform") }.disabled(model.generatingWaveforms || model.scanningSource != nil).help("Prepare waveforms for a track, selected tracks or the whole library")
Menu {
Button("New Singer…") { model.beginSinger() }
@@ -769,6 +774,10 @@ struct LibraryView: View {
updateLibraryRows()
if source == "Singer Songs",presentedSinger != singer { showSinger(singer,section:nil) }
}
.sheet(isPresented:$importingServer,onDismiss:{
let urls = serverImportURLs;serverImportURLs = []
model.importFiles(urls)
}) { ServerImportSheet { serverImportURLs = $0 } }
.fileImporter(isPresented: $importing, allowedContentTypes: [.folder, .audio, .movie, .zip, UTType(filenameExtension: "cdg") ?? .data], allowsMultipleSelection: true) { result in
switch result {
case .success(let urls): model.importFiles(urls)
@@ -1394,6 +1403,58 @@ private struct MusicStoreEditor: View {
}
private func save() { do { try model.saveStore(.init(id:store?.id ?? 0,name:name,url:url,searchTemplate:search));dismiss() } catch { self.error = error.localizedDescription } }
}
/// Finder owns server authentication and stores credentials through macOS.
/// Only the user-selected mounted folder is passed to the ordinary copy importer.
enum SMBImportAddress {
static func url(_ address:String) throws -> URL {
let text = address.trimmingCharacters(in:.whitespacesAndNewlines)
guard let url = URL(string:text),url.scheme?.lowercased() == "smb",
let host = url.host,!host.isEmpty,url.user == nil,url.password == nil,
url.query == nil,url.fragment == nil else {
throw LibraryError.message("Enter an SMB address such as smb://nas/Music. Leave usernames and passwords out; macOS will ask you to sign in.")
}
return url
}
}
private struct ServerImportSheet: View {
let selected: ([URL]) -> Void
@Environment(\.dismiss) private var dismiss
@State private var address = "smb://"
@State private var choosingFolder = false
@State private var error: String?
var body: some View {
VStack(alignment:.leading,spacing:16) {
Text("Import from SMB/NAS").font(.title2.bold())
Text("Connect to your server, then choose a folder on its mounted drive. Supported files in that folder and its subfolders will be copied into your media library.").fixedSize(horizontal:false,vertical:true)
TextField("smb://nas/Music",text:$address).textFieldStyle(.roundedBorder)
Button("Connect in Finder…") {
do {
let url = try SMBImportAddress.url(address)
guard NSWorkspace.shared.open(url) else { throw LibraryError.message("Could not open the server connection in Finder.") }
error = nil
} catch { self.error = error.localizedDescription }
}
Text("Finish signing in through macOS. Then use Choose Server Folder to select the connected drive under Locations. You can also choose a drive that is already connected.").font(.callout).foregroundStyle(.secondary).fixedSize(horizontal:false,vertical:true)
if let error { Text(error).foregroundStyle(.red).fixedSize(horizontal:false,vertical:true) }
HStack {
Button("Cancel") { dismiss() }.keyboardShortcut(.cancelAction)
Spacer()
Button("Choose Server Folder…") {
choosingFolder = true
}.keyboardShortcut(.defaultAction)
}
}.padding(24).frame(width:500)
.fileImporter(isPresented:$choosingFolder,allowedContentTypes:[.folder],allowsMultipleSelection:false) { result in
switch result {
case .success(let urls):
guard !urls.isEmpty else { return }
selected(urls)
dismiss()
case .failure(let error): self.error = error.localizedDescription
}
}
}
}
private struct SingerSongsSheet: View {
@ObservedObject var model: AppModel
let singer: PersonPlace
@@ -1401,6 +1462,8 @@ private struct SingerSongsSheet: View {
@Environment(\.dismiss) private var dismiss
@State private var search = ""
@State private var importing = false
@State private var importingServer = false
@State private var serverImportURLs = [URL]()
var body: some View {
VStack(alignment:.leading,spacing:14) {
Text(tonight ? "Tonight for \(singer.name)" : "Songs for \(singer.name)").font(.title2.bold())
@@ -1421,10 +1484,14 @@ private struct SingerSongsSheet: View {
if model.songs.isEmpty { Text("Your library is empty. Import audio or MP3+G below.").foregroundStyle(.secondary) }
}.frame(maxWidth:.infinity,alignment:.leading)
}.frame(height:270)
HStack { Button("Import Songs…") { importing = true };Spacer();Button("Done") { dismiss() }.keyboardShortcut(.defaultAction) }
HStack { Button("Import Songs…") { importing = true };Button("SMB/NAS…") { importingServer = true };Spacer();Button("Done") { dismiss() }.keyboardShortcut(.defaultAction) }
}.padding(24).frame(width:520).background(StudioTheme.frame)
.disabled(model.importProgress != nil || model.storageProgress != nil)
.overlay { if model.storageProgress != nil { ImportProgressDialog(model:model,storageMode:true) } else if model.importProgress != nil { ImportProgressDialog(model:model) } }
.sheet(isPresented:$importingServer,onDismiss:{
let urls = serverImportURLs;serverImportURLs = []
model.importFiles(urls)
}) { ServerImportSheet { serverImportURLs = $0 } }
.fileImporter(isPresented:$importing,allowedContentTypes:[.folder,.audio,.movie,.zip,UTType(filenameExtension:"cdg") ?? .data],allowsMultipleSelection:true) { result in
switch result { case .success(let urls):model.importFiles(urls);case .failure(let error):model.error = error.localizedDescription }
}