feat(player): add Mornye media notification controls

Sync notification favorites with Library, add Mornye transport icons and an output action on legacy Android, and expose the iOS favorite command. Preserve playback position when controls refresh.

Vendor audio_service 0.18.19 with a small Android patch for custom notification actions. Modern Android keeps its system output switcher and transport layout.
This commit is contained in:
zarzet committed 2026-09-26 03:07:38 +07:00
1 parent 728d179d69
commit 5259c86365
80 files changed
+8973 -13

No files matched your search

+2
View File
@@ -24,6 +24,7 @@ import UniformTypeIdentifiers
private var libraryScanProgressGeneration: UInt64 = 0
private var backendChannel: FlutterMethodChannel?
private var concertCalendar: ConcertCalendarBridge?
private var playbackNotification: PlaybackNotificationBridge?
private let coreBackend: CoreBackend = createCoreBackend()
private var pendingSessionGrantEvents: [[String: Any]] = []
@@ -110,6 +111,7 @@ import UniformTypeIdentifiers
)
GeneratedPluginRegistrant.register(with: registry)
playbackNotification = PlaybackNotificationBridge(messenger: messenger)
registry.registrar(forPlugin: "AudioOutputView")?.register(
AudioOutputViewFactory(messenger: messenger),
withId: "com.zarz.spotiflac/audio_output"
+34
View File
@@ -1,7 +1,41 @@
import AVKit
import Flutter
import MediaPlayer
import UIKit
/// iOS owns the lock-screen layout, transport glyphs and AirPlay button.
/// Supply its favorite command with the same state/action as the Mornye player.
final class PlaybackNotificationBridge {
private let channel: FlutterMethodChannel
private var target: Any?
init(messenger: FlutterBinaryMessenger) {
channel = FlutterMethodChannel(name: "com.zarz.spotiflac/playback_notification", binaryMessenger: messenger)
let command = MPRemoteCommandCenter.shared().likeCommand
target = command.addTarget { [weak self] _ in
guard let self else { return .commandFailed }
self.channel.invokeMethod("favorite", arguments: nil)
return .success
}
command.isEnabled = false
channel.setMethodCallHandler { call, result in
guard call.method == "update", let values = call.arguments as? [String: Any] else {
result(FlutterMethodNotImplemented)
return
}
command.isEnabled = values["enabled"] as? Bool ?? false
command.isActive = values["loved"] as? Bool ?? false
command.localizedTitle = values["label"] as? String ?? "Favorite"
result(nil)
}
}
deinit {
if let target { MPRemoteCommandCenter.shared().likeCommand.removeTarget(target) }
channel.setMethodCallHandler(nil)
}
}
final class AudioOutputViewFactory: NSObject, FlutterPlatformViewFactory {
private let messenger: FlutterBinaryMessenger