904b9b3d-c0eb-42f3-acef-958.../Sources/support/ChineseAppDetector.swift

65 lines
2.3 KiB
Swift
Raw Normal View History

v2.0.0: full rewrite with multi-source NowPlaying Replaces the v1.0.0 shell (MediaRemote-only, non-functional on macOS 15.4+) with a layered design that handles four playback sources with sticky source priority routing: NowPlayingState (orchestrator, @MainActor, 3s poll + notifications) ├─ MediaRemote (private framework, dlopen) ├─ Spotify AppleScript (desktop) ├─ Apple Music AppleScript (desktop) └─ Chrome JS injection (YouTube / SoundCloud / web music) UI: - Large album art with color-extracted gradient background - Title / artist / album + source badge - Draggable seek bar with hover-grow affordance - Prev / Play·Pause (56pt lime button) / Next controls - Header slot: 20x20 icon + 3-bar pseudo-spectrum that pulses while playing - Bi-lingual (zh / en), follows host appLanguage Graceful degradation: - Host < v2.1.7 → upgrade banner (NSAppleEventsUsageDescription required) - QQ Music / NetEase / Kugou detected → "desktop unsupported, try web version" - Empty state with hint to play something in supported apps Build layout: Sources/ root (MioPlugin.swift contract + MusicPlugin principal) Sources/sources/ data sources Sources/ui/ SwiftUI views Sources/support/ ChineseAppDetector / HostVersionCheck / Localization build.sh now recursively finds .swift under Sources/. Breaking-ish: plugin id ("music-player") and bundle ID preserved. Users on v1.0.0 can upgrade in place via the plugin store. Requires: MioIsland host >= v2.1.7 for full functionality. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:27:21 +00:00
//
// ChineseAppDetector.swift
// MioIsland Music Plugin
//
// Detects whether a Chinese desktop music app (QQ / / )
// is currently running. These apps do not publish their Now Playing state
// to MediaRemote and do not expose a scripting dictionary, so we cannot
// read tracks from them. When detected, the UI surfaces a polite message
// telling the user to switch to the web player instead of showing an empty
// or misleading Now Playing view.
//
import AppKit
struct ChineseAppDetector {
private struct Rule {
let bundleIDs: [String]
let nameFragments: [String]
let displayName: String
}
// Bundle IDs are the primary key; localized name fragments are a fallback
// in case the vendor ships a repackaged build with a different bundle ID.
private static let rules: [Rule] = [
Rule(
bundleIDs: ["com.tencent.qqmusicmac", "com.tencent.QQMusicMac"],
nameFragments: ["QQ音乐", "QQ 音乐", "QQMusic"],
displayName: "QQ 音乐"
),
Rule(
bundleIDs: ["com.netease.163music", "com.netease.cloudmusicmac"],
nameFragments: ["网易云音乐", "网易云", "NeteaseMusic", "CloudMusic"],
displayName: "网易云音乐"
),
Rule(
bundleIDs: ["com.kugou.mac", "com.kugou.KuGouMusic"],
nameFragments: ["酷狗", "KuGou"],
displayName: "酷狗音乐"
)
]
/// Return the display name of the first matching running app, or nil.
static func detectRunning() -> String? {
let apps = NSWorkspace.shared.runningApplications
for rule in rules {
for app in apps {
if app.isTerminated { continue }
if let bid = app.bundleIdentifier,
rule.bundleIDs.contains(where: { bid.caseInsensitiveCompare($0) == .orderedSame }) {
return rule.displayName
}
if let name = app.localizedName {
for frag in rule.nameFragments
where name.range(of: frag, options: .caseInsensitive) != nil {
return rule.displayName
}
}
}
}
return nil
}
}