mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Added Amazon Music Support
This commit is contained in:
parent
dbdeabdd83
commit
517de364f7
8 changed files with 566 additions and 22 deletions
|
|
@ -184,6 +184,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
let targetBundleIDs = [
|
||||
"com.apple.Music",
|
||||
"com.spotify.client",
|
||||
"com.amazon.music",
|
||||
"com.apple.Safari",
|
||||
"com.tidal.desktop",
|
||||
"tv.plex.plexamp",
|
||||
|
|
|
|||
287
DynamicIsland/MediaControllers/AmazonMusicController.swift
Normal file
287
DynamicIsland/MediaControllers/AmazonMusicController.swift
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
/*
|
||||
* Atoll (DynamicIsland)
|
||||
* Copyright (C) 2024-2026 Atoll Contributors
|
||||
*
|
||||
* Originally from boring.notch project
|
||||
* Modified and adapted for Atoll (DynamicIsland)
|
||||
* See NOTICE for details.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
/// Media Remote stream filtered to `com.amazon.music` only. When another app owns
|
||||
/// Now Playing, state idles so stale Amazon metadata is not shown.
|
||||
final class AmazonMusicController: ObservableObject, MediaControllerProtocol {
|
||||
static let bundleIdentifier = "com.amazon.music"
|
||||
|
||||
func updatePlaybackInfo() async {}
|
||||
|
||||
@Published private(set) var playbackState: PlaybackState = AmazonMusicController.makeIdlePlaybackState()
|
||||
|
||||
var playbackStatePublisher: AnyPublisher<PlaybackState, Never> {
|
||||
$playbackState.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var isWorking: Bool {
|
||||
process != nil && process?.isRunning == true
|
||||
}
|
||||
|
||||
private let mediaRemoteBundle: CFBundle
|
||||
private let MRMediaRemoteSendCommandFunction: @convention(c) (Int, AnyObject?) -> Void
|
||||
private let MRMediaRemoteSetElapsedTimeFunction: @convention(c) (Double) -> Void
|
||||
private let MRMediaRemoteSetShuffleModeFunction: @convention(c) (Int) -> Void
|
||||
private let MRMediaRemoteSetRepeatModeFunction: @convention(c) (Int) -> Void
|
||||
|
||||
private var process: Process?
|
||||
private var pipeHandler: JSONLinesPipeHandler?
|
||||
private var streamTask: Task<Void, Never>?
|
||||
|
||||
/// True only after a stream line explicitly identified Amazon Music as the now playing source.
|
||||
private var amazonSessionActive = false
|
||||
|
||||
init?() {
|
||||
guard
|
||||
let bundle = CFBundleCreate(
|
||||
kCFAllocatorDefault,
|
||||
NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")),
|
||||
let MRMediaRemoteSendCommandPointer = CFBundleGetFunctionPointerForName(
|
||||
bundle, "MRMediaRemoteSendCommand" as CFString),
|
||||
let MRMediaRemoteSetElapsedTimePointer = CFBundleGetFunctionPointerForName(
|
||||
bundle, "MRMediaRemoteSetElapsedTime" as CFString),
|
||||
let MRMediaRemoteSetShuffleModePointer = CFBundleGetFunctionPointerForName(
|
||||
bundle, "MRMediaRemoteSetShuffleMode" as CFString),
|
||||
let MRMediaRemoteSetRepeatModePointer = CFBundleGetFunctionPointerForName(
|
||||
bundle, "MRMediaRemoteSetRepeatMode" as CFString)
|
||||
else { return nil }
|
||||
|
||||
self.mediaRemoteBundle = bundle
|
||||
MRMediaRemoteSendCommandFunction = unsafeBitCast(
|
||||
MRMediaRemoteSendCommandPointer, to: (@convention(c) (Int, AnyObject?) -> Void).self)
|
||||
MRMediaRemoteSetElapsedTimeFunction = unsafeBitCast(
|
||||
MRMediaRemoteSetElapsedTimePointer, to: (@convention(c) (Double) -> Void).self)
|
||||
MRMediaRemoteSetShuffleModeFunction = unsafeBitCast(
|
||||
MRMediaRemoteSetShuffleModePointer, to: (@convention(c) (Int) -> Void).self)
|
||||
MRMediaRemoteSetRepeatModeFunction = unsafeBitCast(
|
||||
MRMediaRemoteSetRepeatModePointer, to: (@convention(c) (Int) -> Void).self)
|
||||
|
||||
Task { await setupNowPlayingObserver() }
|
||||
}
|
||||
|
||||
deinit {
|
||||
streamTask?.cancel()
|
||||
|
||||
if let pipeHandler = self.pipeHandler {
|
||||
Task { await pipeHandler.close() }
|
||||
}
|
||||
|
||||
if let process = self.process {
|
||||
if process.isRunning {
|
||||
process.terminate()
|
||||
process.waitUntilExit()
|
||||
}
|
||||
}
|
||||
|
||||
self.process = nil
|
||||
self.pipeHandler = nil
|
||||
}
|
||||
|
||||
func play() async {
|
||||
MRMediaRemoteSendCommandFunction(0, nil)
|
||||
}
|
||||
|
||||
func pause() async {
|
||||
MRMediaRemoteSendCommandFunction(1, nil)
|
||||
}
|
||||
|
||||
func togglePlay() async {
|
||||
MRMediaRemoteSendCommandFunction(2, nil)
|
||||
}
|
||||
|
||||
func nextTrack() async {
|
||||
MRMediaRemoteSendCommandFunction(4, nil)
|
||||
}
|
||||
|
||||
func previousTrack() async {
|
||||
MRMediaRemoteSendCommandFunction(5, nil)
|
||||
}
|
||||
|
||||
func seek(to time: Double) async {
|
||||
await MainActor.run {
|
||||
MRMediaRemoteSetElapsedTimeFunction(time)
|
||||
}
|
||||
}
|
||||
|
||||
func isActive() -> Bool {
|
||||
NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == Self.bundleIdentifier }
|
||||
}
|
||||
|
||||
func toggleShuffle() async {
|
||||
MRMediaRemoteSetShuffleModeFunction(playbackState.isShuffled ? 1 : 3)
|
||||
playbackState.isShuffled.toggle()
|
||||
}
|
||||
|
||||
func toggleRepeat() async {
|
||||
let newRepeatMode = (playbackState.repeatMode == .off) ? 3 : (playbackState.repeatMode.rawValue - 1)
|
||||
playbackState.repeatMode = RepeatMode(rawValue: newRepeatMode) ?? .off
|
||||
MRMediaRemoteSetRepeatModeFunction(newRepeatMode)
|
||||
}
|
||||
|
||||
private func setupNowPlayingObserver() async {
|
||||
let process = Process()
|
||||
guard
|
||||
let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl"),
|
||||
let frameworkPath =
|
||||
Bundle.main.resourceURL?
|
||||
.appendingPathComponent("MediaRemoteAdapter.framework")
|
||||
.path
|
||||
else {
|
||||
assertionFailure("Could not find mediaremote-adapter.pl script or framework path")
|
||||
return
|
||||
}
|
||||
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/perl")
|
||||
process.arguments = [scriptURL.path, frameworkPath, "stream"]
|
||||
|
||||
let pipeHandler = JSONLinesPipeHandler()
|
||||
process.standardOutput = await pipeHandler.getPipe()
|
||||
|
||||
let stderrPipe = Pipe()
|
||||
process.standardError = stderrPipe
|
||||
stderrPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
let data = handle.availableData
|
||||
guard !data.isEmpty,
|
||||
let message = String(data: data, encoding: .utf8)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!message.isEmpty
|
||||
else { return }
|
||||
print("AmazonMusicController [stderr]: \(message)")
|
||||
}
|
||||
|
||||
self.process = process
|
||||
self.pipeHandler = pipeHandler
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
streamTask = Task { [weak self] in
|
||||
await self?.processJSONStream()
|
||||
}
|
||||
} catch {
|
||||
assertionFailure("Failed to launch mediaremote-adapter.pl: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func processJSONStream() async {
|
||||
guard let pipeHandler = self.pipeHandler else { return }
|
||||
|
||||
await pipeHandler.readJSONLines(as: NowPlayingUpdate.self) { [weak self] update in
|
||||
await self?.handleAdapterUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
private static func makeIdlePlaybackState() -> PlaybackState {
|
||||
var state = PlaybackState(bundleIdentifier: Self.bundleIdentifier)
|
||||
state.title = "Unknown"
|
||||
state.artist = "Unknown"
|
||||
state.album = ""
|
||||
state.isPlaying = false
|
||||
state.artwork = nil
|
||||
state.duration = 0
|
||||
state.currentTime = 0
|
||||
state.isShuffled = false
|
||||
state.repeatMode = .off
|
||||
state.lastUpdated = Date()
|
||||
return state
|
||||
}
|
||||
|
||||
private func applyIdleBecauseNonAmazonSource() {
|
||||
amazonSessionActive = false
|
||||
playbackState = Self.makeIdlePlaybackState()
|
||||
}
|
||||
|
||||
private func handleAdapterUpdate(_ update: NowPlayingUpdate) async {
|
||||
let payload = update.payload
|
||||
let diff = update.diff ?? false
|
||||
|
||||
let explicitParent = payload.parentApplicationBundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let explicitBundle = payload.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let explicitSource: String? = {
|
||||
if let p = explicitParent, !p.isEmpty { return p }
|
||||
if let b = explicitBundle, !b.isEmpty { return b }
|
||||
return nil
|
||||
}()
|
||||
|
||||
if let source = explicitSource {
|
||||
if source != Self.bundleIdentifier {
|
||||
applyIdleBecauseNonAmazonSource()
|
||||
return
|
||||
}
|
||||
amazonSessionActive = true
|
||||
} else if !diff {
|
||||
applyIdleBecauseNonAmazonSource()
|
||||
return
|
||||
} else if !amazonSessionActive {
|
||||
return
|
||||
}
|
||||
|
||||
var newPlaybackState = PlaybackState(bundleIdentifier: Self.bundleIdentifier)
|
||||
|
||||
newPlaybackState.title = payload.title ?? (diff ? self.playbackState.title : "")
|
||||
newPlaybackState.artist = payload.artist ?? (diff ? self.playbackState.artist : "")
|
||||
newPlaybackState.album = payload.album ?? (diff ? self.playbackState.album : "")
|
||||
newPlaybackState.duration = payload.duration ?? (diff ? self.playbackState.duration : 0)
|
||||
newPlaybackState.currentTime = payload.elapsedTime ?? (diff ? self.playbackState.currentTime : 0)
|
||||
|
||||
if let shuffleMode = payload.shuffleMode {
|
||||
newPlaybackState.isShuffled = shuffleMode != 1
|
||||
} else if !diff {
|
||||
newPlaybackState.isShuffled = false
|
||||
} else {
|
||||
newPlaybackState.isShuffled = self.playbackState.isShuffled
|
||||
}
|
||||
if let repeatModeValue = payload.repeatMode {
|
||||
newPlaybackState.repeatMode = RepeatMode(rawValue: repeatModeValue) ?? .off
|
||||
} else if !diff {
|
||||
newPlaybackState.repeatMode = .off
|
||||
} else {
|
||||
newPlaybackState.repeatMode = self.playbackState.repeatMode
|
||||
}
|
||||
|
||||
if let artworkDataString = payload.artworkData {
|
||||
newPlaybackState.artwork = Data(
|
||||
base64Encoded: artworkDataString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
)
|
||||
} else if !diff {
|
||||
newPlaybackState.artwork = nil
|
||||
}
|
||||
|
||||
if let dateString = payload.timestamp,
|
||||
let date = ISO8601DateFormatter().date(from: dateString) {
|
||||
newPlaybackState.lastUpdated = date
|
||||
} else if !diff {
|
||||
newPlaybackState.lastUpdated = Date()
|
||||
} else {
|
||||
newPlaybackState.lastUpdated = self.playbackState.lastUpdated
|
||||
}
|
||||
|
||||
newPlaybackState.playbackRate = payload.playbackRate ?? (diff ? self.playbackState.playbackRate : 1.0)
|
||||
newPlaybackState.isPlaying = payload.playing ?? (diff ? self.playbackState.isPlaying : false)
|
||||
newPlaybackState.bundleIdentifier = Self.bundleIdentifier
|
||||
|
||||
self.playbackState = newPlaybackState
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +126,7 @@ class AudioTap: NSObject {
|
|||
private let targetBundleIDs = [
|
||||
"com.apple.Music",
|
||||
"com.spotify.client",
|
||||
"com.amazon.music",
|
||||
"com.apple.Safari",
|
||||
"com.tidal.desktop",
|
||||
"tv.plex.plexamp",
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ extension MediaControllerType {
|
|||
return String(localized: "Connects directly to the Apple Music app.")
|
||||
case .youtubeMusic:
|
||||
return String(localized: "Requires a third-party client with API plugin enabled.")
|
||||
case .amazonMusic:
|
||||
return String(localized: "Uses macOS Now Playing when the Amazon Music app is the active media source. Playback controls follow the system Now Playing target. Scrubbing the timeline may not work if the Amazon Music app does not support remote seek.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2497,9 +2497,12 @@ struct Media: View {
|
|||
.foregroundColor(.blue) // Ensures it's visibly a link
|
||||
}
|
||||
} else {
|
||||
Text("'Now Playing' was the only option on previous versions and works with all media apps.")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(String(localized: "'Now Playing' was the only option on previous versions and works with all media apps."))
|
||||
Text(String(localized: "Uses macOS Now Playing when the Amazon Music app is the active media source. Playback controls follow the system Now Playing target. Scrubbing the timeline may not work if the Amazon Music app does not support remote seek."))
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
|
|
@ -3830,9 +3833,7 @@ struct LiveActivitiesSettings: View {
|
|||
|
||||
struct Appearance: View {
|
||||
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
||||
@ObservedObject var webcamManager = WebcamManager.shared
|
||||
@Default(.mirrorShape) var mirrorShape
|
||||
@Default(.selectedCameraID) var selectedCameraID
|
||||
@Default(.sliderColor) var sliderColor
|
||||
@Default(.useMusicVisualizer) var useMusicVisualizer
|
||||
@Default(.customVisualizers) var customVisualizers
|
||||
|
|
@ -4239,22 +4240,6 @@ struct Appearance: View {
|
|||
.tag(MirrorShapeEnum.rectangle)
|
||||
}
|
||||
.settingsHighlight(id: highlightID("Mirror shape"))
|
||||
|
||||
if webcamManager.cameraAvailable {
|
||||
Picker("Mirror Camera", selection: $selectedCameraID) {
|
||||
ForEach(webcamManager.availableCameras, id: \.uniqueID) { device in
|
||||
Text(device.localizedName)
|
||||
.tag(device.uniqueID)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedCameraID) { _, _ in
|
||||
if Defaults[.showMirror] {
|
||||
webcamManager.stopSession()
|
||||
webcamManager.startSession()
|
||||
}
|
||||
}
|
||||
.settingsHighlight(id: highlightID("Mirror Camera"))
|
||||
}
|
||||
Defaults.Toggle(key: .showNotHumanFace) {
|
||||
Text("Idle Animation")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,8 @@ class MusicManager: ObservableObject {
|
|||
newController = SpotifyController()
|
||||
case .youtubeMusic:
|
||||
newController = YouTubeMusicController()
|
||||
case .amazonMusic:
|
||||
newController = AmazonMusicController()
|
||||
}
|
||||
|
||||
// Set up state observation for the new controller
|
||||
|
|
@ -975,6 +977,8 @@ extension MusicManager {
|
|||
return appleMusicPink
|
||||
case .spotify:
|
||||
return spotifyGreen
|
||||
case .amazonMusic:
|
||||
return amazonOrange
|
||||
case .nowPlaying:
|
||||
if let bundleIdentifier,
|
||||
let bundleColor = brandAccentColor(forBundleIdentifier: bundleIdentifier) {
|
||||
|
|
@ -992,6 +996,8 @@ extension MusicManager {
|
|||
return appleMusicPink
|
||||
case "com.spotify.client":
|
||||
return spotifyGreen
|
||||
case AmazonMusicController.bundleIdentifier:
|
||||
return amazonOrange
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
@ -999,6 +1005,7 @@ extension MusicManager {
|
|||
|
||||
private static let appleMusicPink = Color(red: 0.999, green: 0.171, blue: 0.331)
|
||||
private static let spotifyGreen = Color(red: 0.0, green: 0.857, blue: 0.302)
|
||||
private static let amazonOrange = Color(red: 1.0, green: 0.6, blue: 0.0)
|
||||
}
|
||||
|
||||
// MARK: - Album Art Flip Helper
|
||||
|
|
|
|||
|
|
@ -405,6 +405,7 @@ enum MediaControllerType: String, CaseIterable, Identifiable, Defaults.Serializa
|
|||
case appleMusic = "Apple Music"
|
||||
case spotify = "Spotify"
|
||||
case youtubeMusic = "Youtube Music"
|
||||
case amazonMusic = "Amazon Music"
|
||||
|
||||
var id: String { self.rawValue }
|
||||
|
||||
|
|
@ -414,6 +415,7 @@ enum MediaControllerType: String, CaseIterable, Identifiable, Defaults.Serializa
|
|||
case .appleMusic: return String(localized: "Apple Music")
|
||||
case .spotify: return String(localized: "Spotify")
|
||||
case .youtubeMusic: return String(localized: "Youtube Music")
|
||||
case .amazonMusic: return String(localized: "Amazon Music")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -794,7 +796,6 @@ extension Defaults.Keys {
|
|||
//static let alwaysShowTabs = Key<Bool>("alwaysShowTabs", default: true)
|
||||
static let showMirror = Key<Bool>("showMirror", default: false)
|
||||
static let mirrorShape = Key<MirrorShapeEnum>("mirrorShape", default: MirrorShapeEnum.rectangle)
|
||||
static let selectedCameraID = Key<String>("selectedCameraID", default: "")
|
||||
static let settingsIconInNotch = Key<Bool>("settingsIconInNotch", default: true)
|
||||
static let lightingEffect = Key<Bool>("lightingEffect", default: true)
|
||||
static let accentColor = Key<Color>("accentColor", default: Color.blue)
|
||||
|
|
|
|||
|
|
@ -11870,6 +11870,136 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Amazon Music": {
|
||||
"localizations": {
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"cs": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Amazon Music"
|
||||
}
|
||||
},
|
||||
"en-GB": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"fi": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"gu": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"hu": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"ta": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Animation": {
|
||||
"localizations": {
|
||||
"ar": {
|
||||
|
|
@ -88866,6 +88996,136 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Uses macOS Now Playing when the Amazon Music app is the active media source. Playback controls follow the system Now Playing target. Scrubbing the timeline may not work if the Amazon Music app does not support remote seek.": {
|
||||
"localizations": {
|
||||
"ar": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"cs": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Uses macOS Now Playing when the Amazon Music app is the active media source. Playback controls follow the system Now Playing target. Scrubbing the timeline may not work if the Amazon Music app does not support remote seek."
|
||||
}
|
||||
},
|
||||
"en-GB": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"fi": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"gu": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"hu": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"nb": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"sv": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"ta": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"tr": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"uk": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Utilization": {
|
||||
"comment": "A label displayed alongside the utilization percentage in the GPU detail view.",
|
||||
"localizations": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue