mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #414 from sharma2007/feature/Amazon-Music-Support
Added Amazon Music Support
This commit is contained in:
commit
e36a728b58
8 changed files with 567 additions and 4 deletions
|
|
@ -184,6 +184,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
let targetBundleIDs = [
|
let targetBundleIDs = [
|
||||||
"com.apple.Music",
|
"com.apple.Music",
|
||||||
"com.spotify.client",
|
"com.spotify.client",
|
||||||
|
"com.amazon.music",
|
||||||
"com.apple.Safari",
|
"com.apple.Safari",
|
||||||
"com.tidal.desktop",
|
"com.tidal.desktop",
|
||||||
"tv.plex.plexamp",
|
"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 = [
|
private let targetBundleIDs = [
|
||||||
"com.apple.Music",
|
"com.apple.Music",
|
||||||
"com.spotify.client",
|
"com.spotify.client",
|
||||||
|
"com.amazon.music",
|
||||||
"com.apple.Safari",
|
"com.apple.Safari",
|
||||||
"com.tidal.desktop",
|
"com.tidal.desktop",
|
||||||
"tv.plex.plexamp",
|
"tv.plex.plexamp",
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,8 @@ extension MediaControllerType {
|
||||||
return String(localized: "Connects directly to the Apple Music app.")
|
return String(localized: "Connects directly to the Apple Music app.")
|
||||||
case .youtubeMusic:
|
case .youtubeMusic:
|
||||||
return String(localized: "Requires a third-party client with API plugin enabled.")
|
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,7 +2497,10 @@ struct Media: View {
|
||||||
.foregroundColor(.blue) // Ensures it's visibly a link
|
.foregroundColor(.blue) // Ensures it's visibly a link
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text("'Now Playing' was the only option on previous versions and works with all media apps.")
|
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)
|
.foregroundStyle(.secondary)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,8 @@ class MusicManager: ObservableObject {
|
||||||
newController = SpotifyController()
|
newController = SpotifyController()
|
||||||
case .youtubeMusic:
|
case .youtubeMusic:
|
||||||
newController = YouTubeMusicController()
|
newController = YouTubeMusicController()
|
||||||
|
case .amazonMusic:
|
||||||
|
newController = AmazonMusicController()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up state observation for the new controller
|
// Set up state observation for the new controller
|
||||||
|
|
@ -975,6 +977,8 @@ extension MusicManager {
|
||||||
return appleMusicPink
|
return appleMusicPink
|
||||||
case .spotify:
|
case .spotify:
|
||||||
return spotifyGreen
|
return spotifyGreen
|
||||||
|
case .amazonMusic:
|
||||||
|
return amazonOrange
|
||||||
case .nowPlaying:
|
case .nowPlaying:
|
||||||
if let bundleIdentifier,
|
if let bundleIdentifier,
|
||||||
let bundleColor = brandAccentColor(forBundleIdentifier: bundleIdentifier) {
|
let bundleColor = brandAccentColor(forBundleIdentifier: bundleIdentifier) {
|
||||||
|
|
@ -992,6 +996,8 @@ extension MusicManager {
|
||||||
return appleMusicPink
|
return appleMusicPink
|
||||||
case "com.spotify.client":
|
case "com.spotify.client":
|
||||||
return spotifyGreen
|
return spotifyGreen
|
||||||
|
case AmazonMusicController.bundleIdentifier:
|
||||||
|
return amazonOrange
|
||||||
default:
|
default:
|
||||||
return nil
|
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 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 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
|
// MARK: - Album Art Flip Helper
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,7 @@ enum MediaControllerType: String, CaseIterable, Identifiable, Defaults.Serializa
|
||||||
case appleMusic = "Apple Music"
|
case appleMusic = "Apple Music"
|
||||||
case spotify = "Spotify"
|
case spotify = "Spotify"
|
||||||
case youtubeMusic = "Youtube Music"
|
case youtubeMusic = "Youtube Music"
|
||||||
|
case amazonMusic = "Amazon Music"
|
||||||
|
|
||||||
var id: String { self.rawValue }
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
|
@ -414,6 +415,7 @@ enum MediaControllerType: String, CaseIterable, Identifiable, Defaults.Serializa
|
||||||
case .appleMusic: return String(localized: "Apple Music")
|
case .appleMusic: return String(localized: "Apple Music")
|
||||||
case .spotify: return String(localized: "Spotify")
|
case .spotify: return String(localized: "Spotify")
|
||||||
case .youtubeMusic: return String(localized: "Youtube Music")
|
case .youtubeMusic: return String(localized: "Youtube Music")
|
||||||
|
case .amazonMusic: return String(localized: "Amazon Music")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
"Animation": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"ar": {
|
"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": {
|
"Utilization": {
|
||||||
"comment": "A label displayed alongside the utilization percentage in the GPU detail view.",
|
"comment": "A label displayed alongside the utilization percentage in the GPU detail view.",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue