performance updates now qwith progress on import

This commit is contained in:
Mark Gregory
2026-11-06 09:17:40 +00:00
parent 8d33198d21
commit 48d8ffe804
6 changed files with 253 additions and 13 deletions
+104 -1
View File
@@ -587,6 +587,9 @@ private struct ImportProgressDialog: View {
ZStack {
Color.black.opacity(0.16).ignoresSafeArea().contentShape(Rectangle()).onTapGesture { }
if let progress = storageMode ? model.storageProgress : model.importProgress {
if !storageMode {
ImportProgressPanel(progress:progress,studio:"Karaoke",cancel:{ model.cancelImport() },close:{ model.dismissImport() })
} else {
VStack(alignment:.leading,spacing:16) {
HStack(spacing:12) {
Image(systemName:progress.finished ? (progress.failure == nil ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") : "square.and.arrow.down")
@@ -600,12 +603,112 @@ private struct ImportProgressDialog: View {
else if !progress.finished { ProgressView().controlSize(.small) }
if progress.completed > 0 && !storageMode { Text("\(progress.completed-progress.skipped) imported · \(progress.skipped) already in library").font(.callout.weight(.medium)) }
Text(progress.failure ?? progress.current).font(.callout).foregroundStyle(.secondary).lineLimit(3).frame(maxWidth:.infinity,alignment:.leading)
if progress.finished,!storageMode { ImportLogView(progress:progress).frame(height:240) }
if progress.cancelled || progress.failure != nil { Text(storageMode ? "Original files are kept until their copies and library links are verified." : "Tracks already imported are saved in your library.").font(.caption).foregroundStyle(.secondary) }
HStack { Spacer();if progress.finished { Button("Done") { if storageMode { model.dismissMediaMove() } else { model.dismissImport() } }.keyboardShortcut(.defaultAction) } else { Button(storageMode ? "Cancel Move" : "Cancel Import") { if storageMode { model.cancelMediaMove() } else { model.cancelImport() } }.keyboardShortcut(.cancelAction) } }
}.padding(24).frame(width:380).background(.regularMaterial,in:RoundedRectangle(cornerRadius:14))
}.padding(24).frame(width:storageMode || !progress.finished ? 380 : 660).background(.regularMaterial,in:RoundedRectangle(cornerRadius:14))
.overlay(RoundedRectangle(cornerRadius:14).strokeBorder(.primary.opacity(0.08)))
.shadow(color:.black.opacity(0.2),radius:24,y:8)
.accessibilityElement(children:.contain).accessibilityLabel(storageMode ? "Media move progress" : "Music import progress")
}
}
}
}
}
struct ImportProgressPanel: View {
let progress: ImportProgress
let studio: String
let cancel: () -> Void
let close: () -> Void
var background: (() -> Void)? = nil
private var fraction: Double { progress.total > 0 ? min(1,max(0,Double(progress.completed)/Double(progress.total))) : 0 }
private var accent: Color { progress.failure != nil || progress.cancelled ? .orange : progress.finished ? .green : .cyan }
private var title: String { progress.finished ? (progress.cancelled ? "Import Cancelled" : progress.failure == nil ? "Import Complete" : "Import Needs Attention") : "Importing Music" }
var body: some View {
VStack(alignment:.leading,spacing:20) {
HStack(spacing:14) {
Image(systemName:progress.finished ? (progress.failure != nil || progress.cancelled ? "exclamationmark.circle.fill" : "checkmark.circle.fill") : "square.and.arrow.down.fill")
.font(.system(size:26,weight:.medium)).foregroundStyle(accent).frame(width:52,height:52)
.background(accent.opacity(0.12),in:RoundedRectangle(cornerRadius:12))
VStack(alignment:.leading,spacing:4) {
Text(studio.uppercased()+" LIBRARY").font(.system(size:10,weight:.semibold)).tracking(1.4).foregroundStyle(.secondary)
Text(title).font(.system(size:22,weight:.semibold))
}
Spacer()
if !progress.finished { ProgressView().controlSize(.small).tint(accent) }
}
VStack(alignment:.leading,spacing:9) {
HStack {
Text(progress.total > 0 ? "\(progress.completed.formatted()) of \(progress.total.formatted()) tracks checked" : "Scanning files and folders…").font(.callout)
Spacer()
Text(progress.total > 0 ? "\(Int(fraction*100))%" : "Preparing").font(.callout.weight(.semibold)).monospacedDigit().foregroundStyle(accent)
}
GeometryReader { proxy in
ZStack(alignment:.leading) {
Capsule().fill(.primary.opacity(0.08))
Capsule().fill(LinearGradient(colors:[accent.opacity(0.65),accent],startPoint:.leading,endPoint:.trailing))
.frame(width:progress.total > 0 ? proxy.size.width*fraction : proxy.size.width*0.08)
}
}.frame(height:9).accessibilityLabel("Import progress").accessibilityValue("\(Int(fraction*100)) percent")
Text(progress.failure ?? progress.current).font(.system(size:11)).foregroundStyle(.secondary).lineLimit(2).frame(height:30,alignment:.topLeading).frame(maxWidth:.infinity,alignment:.leading)
}
HStack(spacing:12) {
count("Imported",value:max(0,progress.completed-progress.skipped),color:.green)
count("Skipped",value:progress.skipped,color:.orange)
count("Remaining",value:progress.total > 0 ? max(0,progress.total-progress.completed) : -1,color:.cyan)
}
ImportLogView(progress:progress).frame(height:260)
Divider()
HStack {
Text(progress.finished ? "Review the log or copy it before closing." : "You can cancel safely; completed tracks stay in your library.").font(.caption).foregroundStyle(.secondary)
Spacer()
if progress.finished {
Button("Done",action:close).buttonStyle(.borderedProminent).tint(accent).keyboardShortcut(.defaultAction)
} else {
if let background { Button("Run in Background",action:background) }
Button("Cancel Import",action:cancel).keyboardShortcut(.cancelAction)
}
}
}.padding(26).frame(width:660)
.background(Color(nsColor:.windowBackgroundColor),in:RoundedRectangle(cornerRadius:16))
.overlay(RoundedRectangle(cornerRadius:16).strokeBorder(.primary.opacity(0.1)))
.shadow(color:.black.opacity(0.2),radius:24,y:8)
.accessibilityElement(children:.contain).accessibilityLabel(studio+" import progress")
}
private func count(_ title: String,value: Int,color: Color) -> some View {
VStack(alignment:.leading,spacing:5) {
Text(value >= 0 ? value.formatted() : "—").font(.system(size:22,weight:.semibold,design:.rounded)).monospacedDigit().foregroundStyle(color)
Text(title).font(.caption).foregroundStyle(.secondary)
}.frame(maxWidth:.infinity,alignment:.leading).padding(12).background(color.opacity(0.07),in:RoundedRectangle(cornerRadius:8))
}
}
struct ImportLogView: View {
let progress: ImportProgress
@State private var follow = true
var body: some View {
let lines = progress.reportLines
VStack(alignment:.leading,spacing:8) {
HStack {
Text("Import Log").font(.headline)
Spacer()
if !progress.finished { Toggle("Follow log",isOn:$follow).toggleStyle(.checkbox).controlSize(.small) }
Button("Copy Log") { NSPasteboard.general.clearContents();NSPasteboard.general.setString(progress.logText,forType:.string) }
}
ScrollViewReader { reader in
ScrollView {
LazyVStack(alignment:.leading,spacing:7) {
ForEach(lines.indices,id:\.self) { index in
let line = lines[index]
HStack(alignment:.top,spacing:8) {
Circle().fill(line.hasPrefix("[FAILED]") ? Color.red : line.hasPrefix("[SKIPPED]") ? Color.orange : Color.green).frame(width:5,height:5).padding(.top,5)
Text(line).font(.system(size:11,design:.monospaced)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading)
}.id(index)
}
if lines.isEmpty { Text(progress.finished ? "No tracks processed." : "Waiting for the first track…").font(.callout).foregroundStyle(.secondary) }
}.padding(12)
}.background(Color(nsColor:.textBackgroundColor),in:RoundedRectangle(cornerRadius:8))
.overlay(RoundedRectangle(cornerRadius:8).strokeBorder(.primary.opacity(0.06)))
.onChange(of:lines.count) { _ in if follow,let last = lines.indices.last { reader.scrollTo(last,anchor:.bottom) } }
}
}
}