diff --git a/Widget/NowPlayingWidgetViews.swift b/Widget/NowPlayingWidgetViews.swift
index c8ca4ff..f8da379 100644
--- a/Widget/NowPlayingWidgetViews.swift
+++ b/Widget/NowPlayingWidgetViews.swift
@@ -146,8 +146,6 @@ struct WaveformBar: View {
var body: some View {
GeometryReader { geo in
- let w = geo.size.width
- let barWidth: CGFloat = max((w - CGFloat(barCount - 1) * 1.5) / CGFloat(barCount), 1.5)
let playedCount = Int(Double(barCount) * progress)
ZStack {
diff --git a/iOS/Data/BackupManager.swift b/iOS/Data/BackupManager.swift
index 4188230..0ece00c 100644
--- a/iOS/Data/BackupManager.swift
+++ b/iOS/Data/BackupManager.swift
@@ -115,10 +115,24 @@ class BackupManager {
// 5. Visualizer settings
let visSettings = VisualizerSettings.shared
let visDict: [String: Any] = [
+ "enabled": visSettings.enabled,
+ "nowPlayingEnabled": visSettings.nowPlayingEnabled,
+ "miniPlayerEnabled": visSettings.miniPlayerEnabled,
"realAudioAnalysis": visSettings.realAudioAnalysis,
- "barCount": visSettings.barCount,
- "gain": visSettings.gain,
- "colorScheme": visSettings.colorScheme,
+ "dynamicGainEnabled": visSettings.dynamicGainEnabled,
+ "fps": visSettings.fps,
+ "viscosity": visSettings.viscosity,
+ "frequencyCutoff": visSettings.frequencyCutoff,
+ "baseMultiplier": visSettings.baseMultiplier,
+ "waveStrokeThickness": visSettings.waveStrokeThickness,
+ "barSpacing": visSettings.barSpacing,
+ "barCornerRadius": visSettings.barCornerRadius,
+ "lineThickness": visSettings.lineThickness,
+ "nowPlayingHeightPct": visSettings.nowPlayingHeightPct,
+ "waveOffsetTop": visSettings.waveOffsetTop,
+ "npAmplitude": visSettings.npAmplitude,
+ "npBaseLift": visSettings.npBaseLift,
+ "depthOffset": visSettings.depthOffset,
]
let visData = try JSONSerialization.data(withJSONObject: visDict, options: .prettyPrinted)
try visData.write(to: tempDir.appendingPathComponent("visualizer_settings.json"))
@@ -243,10 +257,24 @@ class BackupManager {
let data = try? Data(contentsOf: visURL),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
let vis = VisualizerSettings.shared
- if let v = dict["realAudioAnalysis"] as? Bool { vis.realAudioAnalysis = v }
- if let v = dict["barCount"] as? Int { vis.barCount = v }
- if let v = dict["gain"] as? Double { vis.gain = Float(v) }
- if let v = dict["colorScheme"] as? String { vis.colorScheme = v }
+ if let v = dict["enabled"] as? Bool { vis.enabled = v }
+ if let v = dict["nowPlayingEnabled"] as? Bool { vis.nowPlayingEnabled = v }
+ if let v = dict["miniPlayerEnabled"] as? Bool { vis.miniPlayerEnabled = v }
+ if let v = dict["realAudioAnalysis"] as? Bool { vis.realAudioAnalysis = v }
+ if let v = dict["dynamicGainEnabled"] as? Bool { vis.dynamicGainEnabled = v }
+ if let v = dict["fps"] as? Double { vis.fps = v }
+ if let v = dict["viscosity"] as? Double { vis.viscosity = v }
+ if let v = dict["frequencyCutoff"] as? Int { vis.frequencyCutoff = v }
+ if let v = dict["baseMultiplier"] as? Double { vis.baseMultiplier = v }
+ if let v = dict["waveStrokeThickness"] as? Double { vis.waveStrokeThickness = v }
+ if let v = dict["barSpacing"] as? Double { vis.barSpacing = v }
+ if let v = dict["barCornerRadius"] as? Double { vis.barCornerRadius = v }
+ if let v = dict["lineThickness"] as? Double { vis.lineThickness = v }
+ if let v = dict["nowPlayingHeightPct"] as? Double { vis.nowPlayingHeightPct = v }
+ if let v = dict["waveOffsetTop"] as? Double { vis.waveOffsetTop = v }
+ if let v = dict["npAmplitude"] as? Double { vis.npAmplitude = v }
+ if let v = dict["npBaseLift"] as? Double { vis.npBaseLift = v }
+ if let v = dict["depthOffset"] as? Double { vis.depthOffset = v }
}
// 6. Restore playback state
diff --git a/iOS/Data/PendingOperationsQueue.swift b/iOS/Data/PendingOperationsQueue.swift
index 10c1172..ec1483e 100644
--- a/iOS/Data/PendingOperationsQueue.swift
+++ b/iOS/Data/PendingOperationsQueue.swift
@@ -122,16 +122,19 @@ class PendingOperationsQueue: ObservableObject {
}
}
+ // Copy to let for safe capture in MainActor.run (Swift 6 concurrency)
+ let finalRemaining = remaining
+
await MainActor.run {
- self.operations = remaining
+ self.operations = finalRemaining
self.saveToDisk()
self.isProcessing = false
- if remaining.isEmpty {
+ if finalRemaining.isEmpty {
DebugLogger.shared.log("All pending ops completed", category: "PendingOps")
} else {
DebugLogger.shared.log(
- "\(remaining.count) ops still pending after retry",
+ "\(finalRemaining.count) ops still pending after retry",
category: "PendingOps"
)
}
@@ -140,7 +143,7 @@ class PendingOperationsQueue: ObservableObject {
}
private func retryOperation(_ op: PendingOperation) async -> Bool {
- guard let api = try? CompanionAPIService() else { return false }
+ let api = CompanionAPIService.shared
switch op.type {
case .metadataEdit:
diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist
index ca38d74..1fe5504 100644
--- a/iOS/Resources/Info.plist
+++ b/iOS/Resources/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0
+ $(MARKETING_VERSION)
CFBundleVersion
- 1
+ $(CURRENT_PROJECT_VERSION)
LSRequiresIPhoneOS
diff --git a/iOS/Views/Library/DownloadsSettingsView.swift b/iOS/Views/Library/DownloadsSettingsView.swift
index 4553569..4cb9d55 100644
--- a/iOS/Views/Library/DownloadsSettingsView.swift
+++ b/iOS/Views/Library/DownloadsSettingsView.swift
@@ -726,6 +726,15 @@ struct SettingsView: View {
Spacer()
Text("1.16.1").foregroundColor(.gray)
}
+ NavigationLink {
+ LicensesView()
+ } label: {
+ HStack {
+ Image(systemName: "doc.text")
+ .foregroundStyle(accentPink)
+ Text("Licenses & Acknowledgments")
+ }
+ }
}
// Logout
diff --git a/iOS/Views/Library/LicensesView.swift b/iOS/Views/Library/LicensesView.swift
new file mode 100644
index 0000000..b30d476
--- /dev/null
+++ b/iOS/Views/Library/LicensesView.swift
@@ -0,0 +1,172 @@
+import SwiftUI
+
+// ──────────────────────────────────────────────────────────────────────
+// LicensesView.swift
+// Displays all open-source licenses for dependencies used in the app
+// and the Companion API server.
+// ──────────────────────────────────────────────────────────────────────
+
+struct LicenseItem: Identifiable {
+ let id = UUID()
+ let name: String
+ let description: String
+ let license: String
+ let url: String
+ let category: String // "App" or "Companion API"
+}
+
+struct LicensesView: View {
+ private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333)
+
+ private let licenses: [LicenseItem] = [
+ // ── iOS App ──
+ LicenseItem(
+ name: "ZIPFoundation",
+ description: "Effortless ZIP handling in Swift",
+ license: "MIT License",
+ url: "https://github.com/weichsel/ZIPFoundation",
+ category: "App"
+ ),
+
+ // ── Companion API (Python) ──
+ LicenseItem(
+ name: "FastAPI",
+ description: "Modern, fast web framework for building APIs with Python",
+ license: "MIT License",
+ url: "https://github.com/tiangolo/fastapi",
+ category: "Companion API"
+ ),
+ LicenseItem(
+ name: "Uvicorn",
+ description: "Lightning-fast ASGI server for Python",
+ license: "BSD License",
+ url: "https://github.com/encode/uvicorn",
+ category: "Companion API"
+ ),
+ LicenseItem(
+ name: "Mutagen",
+ description: "Python module to handle audio metadata",
+ license: "GPL-2.0 License",
+ url: "https://github.com/quodlibet/mutagen",
+ category: "Companion API"
+ ),
+ LicenseItem(
+ name: "httpx",
+ description: "Fully featured HTTP client for Python 3",
+ license: "BSD License",
+ url: "https://github.com/encode/httpx",
+ category: "Companion API"
+ ),
+ LicenseItem(
+ name: "NumPy",
+ description: "Fundamental package for scientific computing with Python",
+ license: "BSD License",
+ url: "https://github.com/numpy/numpy",
+ category: "Companion API"
+ ),
+ LicenseItem(
+ name: "Pydantic",
+ description: "Data validation using Python type annotations",
+ license: "MIT License",
+ url: "https://github.com/pydantic/pydantic",
+ category: "Companion API"
+ ),
+ LicenseItem(
+ name: "librosa",
+ description: "Python library for audio and music analysis",
+ license: "ISC License",
+ url: "https://github.com/librosa/librosa",
+ category: "Companion API"
+ ),
+ ]
+
+ private var appLicenses: [LicenseItem] {
+ licenses.filter { $0.category == "App" }
+ }
+
+ private var companionLicenses: [LicenseItem] {
+ licenses.filter { $0.category == "Companion API" }
+ }
+
+ var body: some View {
+ List {
+ Section {
+ VStack(alignment: .leading, spacing: 6) {
+ Text("NavidromePlayer uses the following open-source libraries. We're grateful to the developers and communities behind these projects.")
+ .font(.system(size: 13))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 4)
+ }
+
+ Section("App") {
+ ForEach(appLicenses) { item in
+ licenseRow(item)
+ }
+ }
+
+ Section("Companion API") {
+ ForEach(companionLicenses) { item in
+ licenseRow(item)
+ }
+ }
+
+ Section {
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Visualizer")
+ .font(.system(size: 15, weight: .semibold))
+ Text("The audio visualizer is inspired by Mitsuha, originally created by c0ldra1n for jailbroken iOS. This is an independent reimplementation using Apple frameworks.")
+ .font(.system(size: 13))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.vertical, 4)
+ } header: {
+ Text("Acknowledgments")
+ }
+ }
+ .navigationTitle("Licenses")
+ }
+
+ private func licenseRow(_ item: LicenseItem) -> some View {
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Text(item.name)
+ .font(.system(size: 15, weight: .semibold))
+ Spacer()
+ Text(item.license)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundStyle(accentPink)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(
+ Capsule()
+ .fill(accentPink.opacity(0.12))
+ )
+ }
+
+ Text(item.description)
+ .font(.system(size: 13))
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+
+ if let url = URL(string: item.url) {
+ Link(destination: url) {
+ Text(item.url.replacingOccurrences(of: "https://", with: ""))
+ .font(.system(size: 11))
+ .foregroundStyle(.blue.opacity(0.8))
+ .lineLimit(1)
+ .truncationMode(.middle)
+ }
+ }
+ }
+ .padding(.vertical, 4)
+ }
+}
+
+#if DEBUG
+#Preview {
+ NavigationStack {
+ LicensesView()
+ }
+}
+#endif