Live Activities, nudging, unit tests
This commit is contained in:
@@ -187,6 +187,10 @@
|
||||
"comment" : "A separator between the year and the number of tracks in an album.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"★" : {
|
||||
"comment" : "A star emoji.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"1. Open Music Assistant in a browser" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -1414,6 +1418,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Keep Mobile MA Growing" : {
|
||||
"comment" : "A title of a view that asks the user to support development.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Language" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -1502,6 +1510,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Lock Screen & Dynamic Island" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sperrbildschirm & Dynamic Island"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Pantalla de bloqueo & Dynamic Island"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Écran verrouillé & Dynamic Island"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Long-Lived Access Token" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -1546,10 +1576,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Maybe Later" : {
|
||||
"comment" : "A button that dismisses the support nudge.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Mobile MA" : {
|
||||
"comment" : "The name of the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Mobile MA is a free, passion-driven app. If it brings music to your life, a small donation helps keep it alive and growing." : {
|
||||
"comment" : "A description of the benefits of supporting the development of Mobile MA.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Music Assistant" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
@@ -1986,7 +2024,6 @@
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Now Playing" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
@@ -2563,6 +2600,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Shows the current track on the Lock Screen and in the Dynamic Island." : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Zeigt den aktuellen Titel auf dem Sperrbildschirm und in der Dynamic Island an."
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Muestra la pista actual en la pantalla de bloqueo y en la Dynamic Island."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Affiche la piste en cours sur l'écran verrouillé et dans la Dynamic Island."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Shuffle" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -2654,6 +2713,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Supporter" : {
|
||||
|
||||
},
|
||||
"Synced to: %@" : {
|
||||
"localizations" : {
|
||||
@@ -2812,6 +2874,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Thank you! ♥" : {
|
||||
|
||||
},
|
||||
"This will delete all locally cached artwork and library data. The next launch or reload may take longer while everything is fetched again." : {
|
||||
"localizations" : {
|
||||
|
||||
@@ -149,6 +149,11 @@ final class MAPlayerManager {
|
||||
/// Finds the best currently-playing player and pushes its state to the Live Activity.
|
||||
/// Spawns a Task to fetch artwork with auth before updating.
|
||||
private func updateLiveActivity() {
|
||||
guard UserDefaults.standard.object(forKey: "liveActivityEnabled") as? Bool ?? true else {
|
||||
liveActivityManager.end()
|
||||
return
|
||||
}
|
||||
|
||||
let playing = players.values
|
||||
.filter { $0.state == .playing }
|
||||
.first { $0.currentItem != nil || playerQueues[$0.playerId]?.currentItem != nil }
|
||||
@@ -248,6 +253,9 @@ final class MAPlayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposed for unit testing only.
|
||||
static func testResizeAndEncode(_ image: UIImage) -> Data? { resizeAndEncode(image) }
|
||||
|
||||
private static func resizeAndEncode(_ image: UIImage) -> Data? {
|
||||
// ActivityKit ContentState limit is 4 KB total (Data fields are base64 in the payload).
|
||||
// 40×40 JPEG at 0.3 quality ≈ 400–700 bytes, well within limits.
|
||||
|
||||
@@ -55,6 +55,7 @@ final class MAWebSocketClient {
|
||||
// MARK: - Properties
|
||||
|
||||
private(set) var connectionState: ConnectionState = .disconnected
|
||||
var isConnected: Bool { connectionState == .connected }
|
||||
private var webSocketTask: URLSessionWebSocketTask?
|
||||
private let session: URLSession
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ private let syncLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Mobi
|
||||
|
||||
struct MainTabView: View {
|
||||
@Environment(MAService.self) private var service
|
||||
@Environment(MAStoreManager.self) private var storeManager
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var selectedTab: String = "library"
|
||||
|
||||
@@ -40,6 +41,7 @@ struct MainTabView: View {
|
||||
Tab("Settings", systemImage: "gear", value: "settings") {
|
||||
SettingsView()
|
||||
}
|
||||
.badge(storeManager.hasEverSupported ? Text("★") : nil)
|
||||
}
|
||||
.withToast()
|
||||
.task {
|
||||
@@ -597,6 +599,7 @@ struct SettingsView: View {
|
||||
@Environment(MAStoreManager.self) private var storeManager
|
||||
@State private var showThankYou = false
|
||||
@State private var showClearCacheConfirm = false
|
||||
@AppStorage("liveActivityEnabled") private var liveActivityEnabled = true
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -679,6 +682,22 @@ struct SettingsView: View {
|
||||
Text("Connection")
|
||||
}
|
||||
|
||||
// Now Playing Section
|
||||
Section {
|
||||
Toggle(isOn: $liveActivityEnabled) {
|
||||
Label("Lock Screen & Dynamic Island", systemImage: "music.note.list")
|
||||
}
|
||||
.onChange(of: liveActivityEnabled) { _, enabled in
|
||||
if !enabled {
|
||||
service.playerManager.liveActivityManager.end()
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Now Playing")
|
||||
} footer: {
|
||||
Text("Shows the current track on the Lock Screen and in the Dynamic Island.")
|
||||
}
|
||||
|
||||
// Actions Section
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
@@ -698,6 +717,20 @@ struct SettingsView: View {
|
||||
|
||||
// Support Development Section
|
||||
Section {
|
||||
// Supporter badge row
|
||||
if storeManager.hasEverSupported {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundStyle(.orange)
|
||||
Text("Supporter")
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundStyle(.orange)
|
||||
Spacer()
|
||||
Text("Thank you! ♥")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let loadError = storeManager.loadError {
|
||||
Label(loadError, systemImage: "exclamationmark.triangle")
|
||||
.font(.caption)
|
||||
|
||||
@@ -10,10 +10,12 @@ import UIKit
|
||||
|
||||
struct RootView: View {
|
||||
@Environment(MAService.self) private var service
|
||||
@Environment(MAStoreManager.self) private var storeManager
|
||||
|
||||
@State private var isInitializing = true
|
||||
@State private var loadingProgress: Double = 0.0
|
||||
@State private var loadingPhase: String = "Starting…"
|
||||
@State private var showNudge = false
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -32,9 +34,26 @@ struct RootView: View {
|
||||
.animation(.easeInOut(duration: 0.4), value: service.isConnected)
|
||||
.applyTheme()
|
||||
.applyLocale()
|
||||
.sheet(isPresented: $showNudge, onDismiss: {
|
||||
storeManager.recordNudgeShown()
|
||||
}) {
|
||||
SupportNudgeView(isPresented: $showNudge)
|
||||
}
|
||||
.task {
|
||||
await initializeConnection()
|
||||
}
|
||||
.onChange(of: isInitializing) { _, initializing in
|
||||
if !initializing { checkNudge() }
|
||||
}
|
||||
}
|
||||
|
||||
private func checkNudge() {
|
||||
guard storeManager.shouldShowNudge else { return }
|
||||
// Small delay so the main UI settles before the sheet appears
|
||||
Task {
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
showNudge = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
Reference in New Issue
Block a user