mirror of
https://github.com/xmqywx/mio-plugin-music.git
synced 2026-04-12 03:14:32 +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