mirror of
https://github.com/xmqywx/mio-plugin-music.git
synced 2026-04-11 17:34:33 +00:00
feat: MioIsland music player plugin — reads system NowPlaying
This commit is contained in:
commit
d044a6f0c0
24
Info.plist
Normal file
24
Info.plist
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>MusicPlugin</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.mioisland.plugin.music-player</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Music Player</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>MusicPlugin.MusicPlugin</string>
|
||||
</dict>
|
||||
</plist>
|
||||
21
Sources/MioPlugin.swift
Normal file
21
Sources/MioPlugin.swift
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// MioPlugin.swift
|
||||
// MioIsland Plugin SDK
|
||||
//
|
||||
// Duplicate of the protocol from the host app. At runtime, @objc
|
||||
// protocol conformance is matched by selector signatures, not by
|
||||
// module identity, so this standalone copy works for .bundle plugins.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
@objc protocol MioPlugin: AnyObject {
|
||||
var id: String { get }
|
||||
var name: String { get }
|
||||
var icon: String { get }
|
||||
var version: String { get }
|
||||
func activate()
|
||||
func deactivate()
|
||||
func makeView() -> NSView
|
||||
@objc optional func viewForSlot(_ slot: String, context: [String: Any]) -> NSView?
|
||||
}
|
||||
54
Sources/MusicHeaderButton.swift
Normal file
54
Sources/MusicHeaderButton.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// MusicHeaderButton.swift
|
||||
// MioIsland Music Plugin
|
||||
//
|
||||
// Small button for the "header" slot — shows a music note icon
|
||||
// that posts a notification to open the music plugin view.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// Notification name that the host app listens for to navigate to a plugin.
|
||||
/// The userInfo dict contains ["pluginId": String].
|
||||
extension Notification.Name {
|
||||
static let openPlugin = Notification.Name("com.codeisland.openPlugin")
|
||||
}
|
||||
|
||||
struct MusicHeaderButtonView: View {
|
||||
@ObservedObject var bridge = NowPlayingBridge.shared
|
||||
@State private var isHovered = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
NotificationCenter.default.post(
|
||||
name: .openPlugin,
|
||||
object: nil,
|
||||
userInfo: ["pluginId": "music-player"]
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "music.note")
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(
|
||||
isHovered
|
||||
? Color(red: 1.0, green: 0.4, blue: 0.6) // 荧光粉色
|
||||
: (bridge.info.isPlaying ? .white.opacity(0.8) : .white.opacity(0.4))
|
||||
)
|
||||
.scaleEffect(isHovered ? 1.15 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.15), value: isHovered)
|
||||
.frame(width: 20, height: 20)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.onHover { hovering in
|
||||
isHovered = hovering
|
||||
if hovering {
|
||||
NSCursor.pointingHand.push()
|
||||
} else {
|
||||
NSCursor.pop()
|
||||
}
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
116
Sources/MusicPlayerView.swift
Normal file
116
Sources/MusicPlayerView.swift
Normal file
@ -0,0 +1,116 @@
|
||||
//
|
||||
// MusicPlayerView.swift
|
||||
// MioIsland Music Plugin
|
||||
//
|
||||
// Compact Now Playing UI designed for the notch panel.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MusicPlayerView: View {
|
||||
@ObservedObject var bridge = NowPlayingBridge.shared
|
||||
|
||||
var body: some View {
|
||||
if bridge.info.title.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
nowPlayingView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Now Playing
|
||||
|
||||
private var nowPlayingView: some View {
|
||||
HStack(spacing: 12) {
|
||||
// Album art
|
||||
if let artwork = bridge.info.artwork {
|
||||
Image(nsImage: artwork)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 48, height: 48)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(width: 48, height: 48)
|
||||
.overlay(
|
||||
Image(systemName: "music.note")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.white.opacity(0.3))
|
||||
)
|
||||
}
|
||||
|
||||
// Info + controls
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Title
|
||||
Text(bridge.info.title)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundColor(.white.opacity(0.95))
|
||||
.lineLimit(1)
|
||||
|
||||
// Artist
|
||||
Text(bridge.info.artist)
|
||||
.font(.system(size: 10))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.lineLimit(1)
|
||||
|
||||
// Progress bar
|
||||
if bridge.info.duration > 0 {
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 1.5)
|
||||
.fill(Color.white.opacity(0.1))
|
||||
.frame(height: 3)
|
||||
RoundedRectangle(cornerRadius: 1.5)
|
||||
.fill(Color.white.opacity(0.6))
|
||||
.frame(width: geo.size.width * progress, height: 3)
|
||||
}
|
||||
}
|
||||
.frame(height: 3)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
// Playback controls
|
||||
HStack(spacing: 14) {
|
||||
controlButton("backward.fill") { bridge.previousTrack() }
|
||||
controlButton(bridge.info.isPlaying ? "pause.fill" : "play.fill") { bridge.togglePlayPause() }
|
||||
.font(.system(size: 14))
|
||||
controlButton("forward.fill") { bridge.nextTrack() }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
// MARK: - Empty State
|
||||
|
||||
private var emptyState: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "music.note.list")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.25))
|
||||
Text("Nothing playing")
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white.opacity(0.3))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var progress: CGFloat {
|
||||
guard bridge.info.duration > 0 else { return 0 }
|
||||
return CGFloat(bridge.info.elapsedTime / bridge.info.duration)
|
||||
}
|
||||
|
||||
private func controlButton(_ symbol: String, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: symbol)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
46
Sources/MusicPlugin.swift
Normal file
46
Sources/MusicPlugin.swift
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// MusicPlugin.swift
|
||||
// MioIsland Music Plugin
|
||||
//
|
||||
// Principal class for the music-player.bundle plugin.
|
||||
// Shows system Now Playing info (Spotify, Apple Music, etc.)
|
||||
// with playback controls in the notch.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
final class MusicPlugin: NSObject, MioPlugin {
|
||||
var id: String { "music-player" }
|
||||
var name: String { "Music Player" }
|
||||
var icon: String { "music.note" }
|
||||
var version: String { "1.0.0" }
|
||||
|
||||
func activate() {
|
||||
Task { @MainActor in
|
||||
NowPlayingBridge.shared.start()
|
||||
}
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
Task { @MainActor in
|
||||
NowPlayingBridge.shared.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> NSView {
|
||||
NSHostingView(rootView: MusicPlayerView())
|
||||
}
|
||||
|
||||
func viewForSlot(_ slot: String, context: [String: Any]) -> NSView? {
|
||||
switch slot {
|
||||
case "header":
|
||||
let view = NSHostingView(rootView: MusicHeaderButtonView())
|
||||
view.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
|
||||
view.setFrameSize(NSSize(width: 20, height: 20))
|
||||
return view
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
129
Sources/NowPlayingBridge.swift
Normal file
129
Sources/NowPlayingBridge.swift
Normal file
@ -0,0 +1,129 @@
|
||||
//
|
||||
// NowPlayingBridge.swift
|
||||
// MioIsland Music Plugin
|
||||
//
|
||||
// Reads system Now Playing info via private MediaRemote.framework.
|
||||
// Dynamically loads the framework to avoid linking against private APIs.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
|
||||
struct NowPlayingInfo {
|
||||
var title: String = ""
|
||||
var artist: String = ""
|
||||
var album: String = ""
|
||||
var artwork: NSImage?
|
||||
var duration: Double = 0
|
||||
var elapsedTime: Double = 0
|
||||
var isPlaying: Bool = false
|
||||
var bundleId: String? // source app
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class NowPlayingBridge: ObservableObject {
|
||||
static let shared = NowPlayingBridge()
|
||||
|
||||
@Published var info = NowPlayingInfo()
|
||||
|
||||
// MediaRemote function pointers
|
||||
private var MRMediaRemoteGetNowPlayingInfo: (@convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void)?
|
||||
private var MRMediaRemoteSendCommand: (@convention(c) (UInt32, UnsafeMutableRawPointer?) -> Bool)?
|
||||
private var MRMediaRemoteRegisterForNowPlayingNotifications: (@convention(c) (DispatchQueue) -> Void)?
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
init() {
|
||||
loadMediaRemote()
|
||||
}
|
||||
|
||||
// MARK: - Load Private Framework
|
||||
|
||||
private func loadMediaRemote() {
|
||||
let path = "/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote"
|
||||
guard let handle = dlopen(path, RTLD_NOW) else { return }
|
||||
|
||||
if let ptr = dlsym(handle, "MRMediaRemoteGetNowPlayingInfo") {
|
||||
MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(ptr, to: (@convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void).self)
|
||||
}
|
||||
if let ptr = dlsym(handle, "MRMediaRemoteSendCommand") {
|
||||
MRMediaRemoteSendCommand = unsafeBitCast(ptr, to: (@convention(c) (UInt32, UnsafeMutableRawPointer?) -> Bool).self)
|
||||
}
|
||||
if let ptr = dlsym(handle, "MRMediaRemoteRegisterForNowPlayingNotifications") {
|
||||
MRMediaRemoteRegisterForNowPlayingNotifications = unsafeBitCast(ptr, to: (@convention(c) (DispatchQueue) -> Void).self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Start / Stop
|
||||
|
||||
func start() {
|
||||
// Register for notifications
|
||||
MRMediaRemoteRegisterForNowPlayingNotifications?(DispatchQueue.main)
|
||||
|
||||
// Listen for changes
|
||||
let nc = NotificationCenter.default
|
||||
let names = [
|
||||
"kMRMediaRemoteNowPlayingInfoDidChangeNotification",
|
||||
"kMRMediaRemoteNowPlayingPlaybackQueueChangedNotification",
|
||||
"kMRMediaRemoteNowPlayingApplicationIsPlayingDidChangeNotification"
|
||||
]
|
||||
for name in names {
|
||||
nc.addObserver(self, selector: #selector(nowPlayingChanged), name: NSNotification.Name(name), object: nil)
|
||||
}
|
||||
|
||||
// Also poll every 2 seconds for elapsed time updates
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in self?.fetchNowPlaying() }
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
fetchNowPlaying()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func nowPlayingChanged() {
|
||||
Task { @MainActor in fetchNowPlaying() }
|
||||
}
|
||||
|
||||
// MARK: - Fetch
|
||||
|
||||
private func fetchNowPlaying() {
|
||||
MRMediaRemoteGetNowPlayingInfo?(DispatchQueue.main) { [weak self] dict in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
var info = NowPlayingInfo()
|
||||
info.title = dict["kMRMediaRemoteNowPlayingInfoTitle"] as? String ?? ""
|
||||
info.artist = dict["kMRMediaRemoteNowPlayingInfoArtist"] as? String ?? ""
|
||||
info.album = dict["kMRMediaRemoteNowPlayingInfoAlbum"] as? String ?? ""
|
||||
info.duration = dict["kMRMediaRemoteNowPlayingInfoDuration"] as? Double ?? 0
|
||||
info.elapsedTime = dict["kMRMediaRemoteNowPlayingInfoElapsedTime"] as? Double ?? 0
|
||||
info.isPlaying = (dict["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Double ?? 0) > 0
|
||||
|
||||
if let artworkData = dict["kMRMediaRemoteNowPlayingInfoArtworkData"] as? Data {
|
||||
info.artwork = NSImage(data: artworkData)
|
||||
}
|
||||
|
||||
self.info = info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Controls (command IDs from MediaRemote.h)
|
||||
|
||||
func togglePlayPause() {
|
||||
_ = MRMediaRemoteSendCommand?(2, nil) // kMRTogglePlayPause
|
||||
}
|
||||
|
||||
func nextTrack() {
|
||||
_ = MRMediaRemoteSendCommand?(4, nil) // kMRNextTrack
|
||||
}
|
||||
|
||||
func previousTrack() {
|
||||
_ = MRMediaRemoteSendCommand?(5, nil) // kMRPreviousTrack
|
||||
}
|
||||
}
|
||||
34
build.sh
Executable file
34
build.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
# Build the Music Player plugin as a .bundle for MioIsland
|
||||
set -e
|
||||
|
||||
PLUGIN_NAME="music-player"
|
||||
BUNDLE_NAME="${PLUGIN_NAME}.bundle"
|
||||
BUILD_DIR="build"
|
||||
SOURCES="Sources/*.swift"
|
||||
|
||||
echo "Building ${PLUGIN_NAME} plugin..."
|
||||
|
||||
# Clean
|
||||
rm -rf "${BUILD_DIR}"
|
||||
mkdir -p "${BUILD_DIR}/${BUNDLE_NAME}/Contents/MacOS"
|
||||
|
||||
# Compile to dynamic library
|
||||
swiftc \
|
||||
-emit-library \
|
||||
-module-name MusicPlugin \
|
||||
-target arm64-apple-macos15.0 \
|
||||
-sdk $(xcrun --show-sdk-path) \
|
||||
-o "${BUILD_DIR}/${BUNDLE_NAME}/Contents/MacOS/MusicPlugin" \
|
||||
${SOURCES}
|
||||
|
||||
# Copy Info.plist
|
||||
cp Info.plist "${BUILD_DIR}/${BUNDLE_NAME}/Contents/"
|
||||
|
||||
# Ad-hoc sign
|
||||
codesign --force --sign - "${BUILD_DIR}/${BUNDLE_NAME}"
|
||||
|
||||
echo "✓ Built ${BUILD_DIR}/${BUNDLE_NAME}"
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " cp -r ${BUILD_DIR}/${BUNDLE_NAME} ~/.config/codeisland/plugins/"
|
||||
24
build/music-player.bundle/Contents/Info.plist
Normal file
24
build/music-player.bundle/Contents/Info.plist
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>MusicPlugin</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.mioisland.plugin.music-player</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Music Player</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>MusicPlugin.MusicPlugin</string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
build/music-player.bundle/Contents/MacOS/MusicPlugin
Executable file
BIN
build/music-player.bundle/Contents/MacOS/MusicPlugin
Executable file
Binary file not shown.
115
build/music-player.bundle/Contents/_CodeSignature/CodeResources
Normal file
115
build/music-player.bundle/Contents/_CodeSignature/CodeResources
Normal file
@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>files</key>
|
||||
<dict/>
|
||||
<key>files2</key>
|
||||
<dict/>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>^Resources/</key>
|
||||
<true/>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^version.plist$</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>rules2</key>
|
||||
<dict>
|
||||
<key>.*\.dSYM($|/)</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>11</real>
|
||||
</dict>
|
||||
<key>^(.*/)?\.DS_Store$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>2000</real>
|
||||
</dict>
|
||||
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^.*</key>
|
||||
<true/>
|
||||
<key>^Info\.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^PkgInfo$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/</key>
|
||||
<dict>
|
||||
<key>optional</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1000</real>
|
||||
</dict>
|
||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>1100</real>
|
||||
</dict>
|
||||
<key>^Resources/Base\.lproj/</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>1010</real>
|
||||
</dict>
|
||||
<key>^[^/]+$</key>
|
||||
<dict>
|
||||
<key>nested</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>^embedded\.provisionprofile$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
<key>^version\.plist$</key>
|
||||
<dict>
|
||||
<key>weight</key>
|
||||
<real>20</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Loading…
Reference in New Issue
Block a user