904b9b3d-c0eb-42f3-acef-958.../Sources/ui/ExpandedView.swift

434 lines
14 KiB
Swift
Raw Permalink 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
//
// ExpandedView.swift
// MusicPlugin
//
// Main panel view, sized roughly 620x780 by the host. Four states
// rendered in priority order:
// 1. Host version too old (hostVersionOK == false)
// 2. Chinese desktop app running (chineseAppDetected != nil)
// 3. Nothing playing (title.isEmpty)
// 4. Now playing (default)
//
// v2.0.1 layout: compact horizontal hero inspired by SuperIsland's
// NowPlaying. Medium album art on the left, metadata + source badge
// on the right, progress + times inline below, transport controls
// at bottom. Half the vertical footprint of v2.0.0 for the same
// information density.
//
// Background uses a tint extracted from the album art (fades into
// a near-black base). Palette:
// #0A0A0A near-black base
// white text tiers 1.0 / 0.75 / 0.45 / 0.3
// lime #CAFF00 as the single accent color
// 16pt corner on the big art, 8pt on small chips
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
//
import AppKit
import SwiftUI
struct ExpandedView: View {
@ObservedObject private var state = NowPlayingState.shared
/// Tint extracted from the current album art. Updated via
/// AlbumArtColorExtractor whenever the art changes.
@State private var tintColor: NSColor?
private static let lime = Color(
red: 0xCA / 255.0,
green: 0xFF / 255.0,
blue: 0x00 / 255.0
)
private static let ink = Color.white
private static let base = Color(red: 0x0A / 255.0, green: 0x0A / 255.0, blue: 0x0A / 255.0)
// MARK: - Body
var body: some View {
ZStack(alignment: .center) {
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
// V2 Immersive backdrop applied only when actually playing,
// other modes (empty / warning) use the plain near-black base.
if currentMode == .playing {
immersiveBackdrop
.ignoresSafeArea()
}
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
content
.padding(20)
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
// Top-right float-window toggle only when playing.
if currentMode == .playing {
VStack {
HStack {
Spacer()
floatWindowToggle
}
Spacer()
}
.padding(14)
}
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
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Self.base)
.onAppear { refreshTint(for: state.albumArt) }
.onChange(of: state.albumArt?.tiffRepresentation) { _, _ in
refreshTint(for: state.albumArt)
}
.animation(.easeInOut(duration: 0.25), value: currentMode)
}
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
/// V2 Immersive backdrop: blurred enlarged album art + dark gradient
/// overlay (35% 72% 92%). Falls back to a solid base when no art.
@ViewBuilder
private var immersiveBackdrop: some View {
if let art = state.albumArt {
ZStack {
Image(nsImage: art)
.resizable()
.scaledToFill()
.saturation(1.4)
.blur(radius: 40, opaque: true)
.scaleEffect(1.3)
.clipped()
LinearGradient(
gradient: Gradient(stops: [
.init(color: Self.base.opacity(0.35), location: 0.0),
.init(color: Self.base.opacity(0.72), location: 0.55),
.init(color: Self.base.opacity(0.92), location: 1.0)
]),
startPoint: .top,
endPoint: .bottom
)
}
} else {
AlbumArtColorExtractor.backgroundGradient(for: tintColor)
}
}
/// Pin/float button that toggles the desktop lyrics overlay.
/// Icon is static (`pip.enter`) we don't observe the window to keep
/// this view free of a @StateObject dependency. The window itself is
/// the visibility signal.
private var floatWindowToggle: some View {
Button {
DesktopLyricsWindow.shared.toggle()
} label: {
Image(systemName: "pip.enter")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(Self.ink.opacity(0.85))
.frame(width: 28, height: 28)
.background(
Circle().fill(Color.black.opacity(0.35))
)
.overlay(
Circle().strokeBorder(Color.white.opacity(0.12), lineWidth: 0.5)
)
}
.buttonStyle(.plain)
.help(L10n.floatLyricsTooltip)
}
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
// MARK: - State routing
private enum Mode: Equatable {
case hostTooOld
case chineseAppWarning(String)
case empty
case playing
}
private var currentMode: Mode {
if !state.hostVersionOK { return .hostTooOld }
if let name = state.chineseAppDetected, !name.isEmpty {
return .chineseAppWarning(name)
}
if state.title.isEmpty { return .empty }
return .playing
}
@ViewBuilder
private var content: some View {
switch currentMode {
case .hostTooOld:
warningCard(
symbol: "exclamationmark.triangle.fill",
title: L10n.hostUpgradeTitle,
hint: L10n.hostUpgradeHint,
tint: .orange
)
case .chineseAppWarning(let appName):
warningCard(
symbol: "exclamationmark.circle.fill",
title: L10n.chineseAppTitle(appName),
hint: L10n.chineseAppHint,
tint: .yellow
)
case .empty:
emptyCard
case .playing:
playingCard
}
}
// MARK: - Playing card compact horizontal layout
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
private var playingCard: some View {
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
// V2 Immersive layout centered column: cover 120 title artist
// progress controls with outline play button. Matches the
// Claude Design CodeIsland Music.html V2 spec.
VStack(spacing: 14) {
// Large centered album art shadow drops onto blurred backdrop
albumArt
.frame(width: 120, height: 120)
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
VStack(spacing: 4) {
Text(state.title.isEmpty ? L10n.unknownTitle : state.title)
.font(.system(size: 19, weight: .semibold))
.tracking(-0.35)
.foregroundColor(Self.ink)
.lineLimit(1)
.shadow(color: .black.opacity(0.35), radius: 6, y: 2)
Text(state.artist.isEmpty ? L10n.unknownArtist : state.artist)
.font(.system(size: 13))
.foregroundColor(Self.ink.opacity(0.78))
.lineLimit(1)
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
}
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.frame(maxWidth: 360)
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
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
// Source chip (below artist, subtle)
sourceBadge
// Progress bar + times
VStack(spacing: 8) {
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
SeekBar(
progress: state.progress,
duration: state.duration
) { newTime in
state.seek(to: newTime)
}
HStack {
Text(state.formattedElapsed)
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.font(.system(size: 10, design: .monospaced))
.foregroundColor(Self.ink.opacity(0.55))
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
Spacer()
Text(state.formattedDuration)
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.font(.system(size: 10, design: .monospaced))
.foregroundColor(Self.ink.opacity(0.55))
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
}
}
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.padding(.top, 6)
Spacer(minLength: 0)
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
transportControls
}
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.frame(maxWidth: 380)
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
}
private var albumArt: some View {
ZStack {
if let art = state.albumArt {
Image(nsImage: art)
.resizable()
.aspectRatio(contentMode: .fill)
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
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
} else {
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
RoundedRectangle(cornerRadius: 10, style: .continuous)
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
.fill(Color.white.opacity(0.08))
.overlay(
Image(systemName: "music.note")
.font(.system(size: 34, weight: .light))
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
.foregroundColor(Self.ink.opacity(0.35))
)
}
}
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.shadow(color: .black.opacity(0.5), radius: 22, x: 0, y: 10)
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
}
private var sourceBadge: some View {
HStack(spacing: 5) {
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
Circle()
.fill(state.isPlaying ? Self.lime : Self.ink.opacity(0.4))
.frame(width: 5, height: 5)
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
Text(displaySourceName)
.font(.system(size: 10, weight: .medium))
.foregroundColor(Self.ink.opacity(0.7))
}
.padding(.horizontal, 8)
.padding(.vertical, 3)
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
.background(
Capsule().fill(Color.white.opacity(0.08))
)
}
private var displaySourceName: String {
state.sourceName.isEmpty ? "..." : state.sourceName
}
private var transportControls: some View {
HStack(spacing: 28) {
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
transportButton(
symbol: "backward.fill",
size: 16,
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
tooltip: L10n.previousTooltip
) {
state.previousTrack()
}
// Play / pause accent button, slightly smaller than v2.0.0 (48 vs 56)
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
// V2 Immersive: outline play button (1.5px 85% white) lets the
// blurred album backdrop breathe through instead of punching a
// big lime disc that fights the art.
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
Button(action: { state.togglePlayPause() }) {
ZStack {
Circle()
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.strokeBorder(Self.ink.opacity(0.85), lineWidth: 1.5)
.frame(width: 48, height: 48)
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
Image(systemName: state.isPlaying ? "pause.fill" : "play.fill")
v2.2.1: real lyrics via LRCLIB + stream envelope fix + TimelineView vinyl Critical fix · adapter stream was silently empty v2.2.0 parsed the stream subprocess's JSON at the wrong layer. The `stream` mode wraps every emit as: {"type":"data","diff":<bool>,"payload":{title,...}} but Swift was decoding as AdapterStreamPayload directly (the shape used by `get`, which is flat). Result: every `stream rx` had title="" because the real data was nested inside payload, so hasTrack was always false and onUpdate never fired. Users saw "nothing playing" even while Apple Music was running. New AdapterStreamEnvelope decodes the wrapper, extracts payload, and also honours `diff: false` to reset currentInfo before merging (stale fields from the previous track were otherwise leaking). Added bootstrap path · cold start with music already playing When the adapter subprocess is spawned AFTER Apple Music is already playing, the stream's initial emit can be an empty baseline. A parallel one-shot `perl adapter.pl get` at spawn+300ms catches the current track immediately. Added file-based debug log at /tmp/mio-plugin-music-debug.log NSLog / os_log are unreliably filtered on macOS 15, and we can't attach Xcode to a plugin loaded from a signed host. A line-oriented log at a fixed path is the one channel that's always readable post- mortem. Lines include stream rx / bootstrap / parse failures. Real lyrics · LRCLIB integration - New LyricsService fetches synced lyrics via https://lrclib.net/api/get (exact) + /search (fallback), parses LRC format with regex [mm:ss.xx]. In-memory LRU cache (32 entries, 1-hour TTL). Negative-caches "not found" so obscure tracks don't re-hit the API every render. - NowPlayingState gains syncedLyrics + currentLyricIndex @Published. applyAdapterUpdate detects track changes and refreshes lyrics detached; the 1s playback timer updates currentLyricIndex. - DesktopLyricsViews replaces the placeholder text with real lyric text from syncedLyrics[currentLyricIndex ± 1]. Falls back to sensible dots when no lyrics loaded / instrumentals. Bonus · robust vinyl spin via TimelineView withAnimation(.linear.repeatForever) loses the animation when SwiftUI re-creates the view (window hide/show, style switch). Replaced with TimelineView driven by wall-clock — angle = (elapsed * 45°/s) % 360. Smooth across hours, no drift, pauses correctly via `paused: !isPlaying`. Borrowed approach from Atoll (github.com/Ebullioscopic/Atoll) MusicManager.swift:756-935: same LRCLIB endpoints, same LRC regex shape, same per-second sync model. Credited in LICENSE-THIRD-PARTY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:55:04 +00:00
.font(.system(size: 18, weight: .semibold))
.foregroundColor(Self.ink)
.offset(x: state.isPlaying ? 0 : 2)
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
}
}
.buttonStyle(.plain)
.help(state.isPlaying ? L10n.pauseTooltip : L10n.playTooltip)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: state.isPlaying)
transportButton(
symbol: "forward.fill",
size: 16,
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
tooltip: L10n.nextTooltip
) {
state.nextTrack()
}
}
}
private func transportButton(
symbol: String,
size: CGFloat,
tooltip: String,
action: @escaping () -> Void
) -> some View {
TransportIconButton(
symbol: symbol,
size: size,
tooltip: tooltip,
action: action
)
}
// MARK: - Empty card (nothing playing)
private var emptyCard: some View {
VStack(spacing: 12) {
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
Image(systemName: "music.note")
.font(.system(size: 40, weight: .light))
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
.foregroundColor(Self.ink.opacity(0.3))
Text(L10n.nothingPlaying)
.font(.system(size: 16, weight: .semibold))
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
.foregroundColor(Self.ink.opacity(0.7))
Text(L10n.nothingPlayingHint)
.font(.system(size: 11, weight: .regular))
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
.foregroundColor(Self.ink.opacity(0.4))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
.padding(24)
.frame(maxWidth: 320)
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
}
// MARK: - Warning cards (host outdated / chinese app detected)
private func warningCard(
symbol: String,
title: String,
hint: String,
tint: Color
) -> some View {
VStack(spacing: 12) {
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
Image(systemName: symbol)
.font(.system(size: 36, weight: .regular))
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
.foregroundColor(tint)
Text(title)
.font(.system(size: 15, weight: .semibold))
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
.foregroundColor(Self.ink.opacity(0.9))
.multilineTextAlignment(.center)
Text(hint)
.font(.system(size: 11, weight: .regular))
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
.foregroundColor(Self.ink.opacity(0.55))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.lineSpacing(2)
}
.padding(22)
.frame(maxWidth: 340)
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
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
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
.fill(Color.white.opacity(0.04))
)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
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
.stroke(Color.white.opacity(0.08), lineWidth: 0.5)
)
}
// MARK: - Tint refresh
private func refreshTint(for image: NSImage?) {
// Prefer the tint NowPlayingState already computed (if data source
// pushed one), but fall back to extracting here. Either way, we
// re-run extraction so the gradient tracks the current art.
if let stateColor = state.albumArtColor {
tintColor = stateColor
return
}
AlbumArtColorExtractor.extract(from: image) { color in
tintColor = color
}
}
}
// MARK: - Transport icon button
/// Ghost-style round icon button with a lime hover glow. Factored out
/// so it can own its own @State for hover without mutating parent.
private struct TransportIconButton: View {
let symbol: String
let size: CGFloat
let tooltip: String
let action: () -> Void
@State private var isHovered = false
private static let lime = Color(
red: 0xCA / 255.0,
green: 0xFF / 255.0,
blue: 0x00 / 255.0
)
var body: some View {
Button(action: action) {
Image(systemName: symbol)
.font(.system(size: size, weight: .semibold))
.foregroundColor(isHovered ? Self.lime : Color.white.opacity(0.75))
.frame(width: 36, height: 36)
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
.background(
Circle()
.fill(Color.white.opacity(isHovered ? 0.10 : 0.0))
)
.scaleEffect(isHovered ? 1.05 : 1.0)
.animation(.easeInOut(duration: 0.15), value: isHovered)
}
.buttonStyle(.plain)
.help(tooltip)
.onHover { hovering in
isHovered = hovering
}
}
}