904b9b3d-c0eb-42f3-acef-958.../Sources/MusicPlugin.swift
徐翔宇 64daaa3371 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-19 02:27:21 +08:00

72 lines
2.3 KiB
Swift

//
// MusicPlugin.swift
// MioIsland Music Plugin
//
// Principal class for the music-player.bundle plugin (v2.0.0).
//
// Wires together the data layer (NowPlayingState + sources/*) and the UI
// layer (ui/*). Loaded at runtime by the host's NativePluginManager via
// Info.plist -> NSPrincipalClass = "MusicPlugin.MusicPlugin".
//
// v2.0.0 is a complete rewrite of the v1.0.0 shell. The old files
// (NowPlayingBridge / MusicPlayerView / MusicHeaderButton) have been
// replaced by a layered design:
//
// NowPlayingState -> orchestrator + sticky source routing
// +-> sources/MediaRemoteSource (dlopen private framework)
// +-> sources/SpotifyAppleScript
// +-> sources/AppleMusicAppleScript
// +-> sources/ChromeWebSource (JS injection into video/audio)
// +-> support/ChineseAppDetector (QQ / NetEase / Kugou)
// +-> support/HostVersionCheck (host >= 2.1.7 gate)
//
// ui/ExpandedView -> main panel (makeView)
// ui/HeaderSlotView -> 20x20 header icon + pseudo-spectrum
// ui/AlbumArtColorExtractor + ui/SeekBar
// support/Localization (zh/en)
//
import AppKit
import SwiftUI
/// Principal class. Module is `MusicPlugin`, class is `MusicPlugin`, so
/// Info.plist NSPrincipalClass = "MusicPlugin.MusicPlugin".
final class MusicPlugin: NSObject, MioPlugin {
var id: String { "music-player" }
var name: String { "Music Player" }
var icon: String { "music.note" }
var version: String { "2.0.0" }
func activate() {
NSLog("[mio-plugin-music] activate")
Task { @MainActor in
NowPlayingState.shared.start()
}
}
func deactivate() {
NSLog("[mio-plugin-music] deactivate")
Task { @MainActor in
NowPlayingState.shared.stop()
}
}
func makeView() -> NSView {
let view = NSHostingView(rootView: ExpandedView())
view.autoresizingMask = [.width, .height]
return view
}
@objc func viewForSlot(_ slot: String, context: [String: Any]) -> NSView? {
switch slot {
case "header":
let view = NSHostingView(rootView: HeaderSlotView())
view.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
view.setFrameSize(NSSize(width: 20, height: 20))
return view
default:
return nil
}
}
}