Muting does not change the volume level, and VolumeChanged carried nothing but that level — so pressing M silenced playback and left the indicator showing the volume it still had. The UI had nothing to react to. Mute rides on its own event rather than widening the volume payload, since the two are genuinely independent: a muted player at 40% is a different state from a player at 0%, and only one of them comes back when you unmute. The icon crosses out and dims, and the popup gains an explicit Mute/Unmute so the keyboard shortcut is not the only way in. MuteToggle also now takes the speaker lock (it was mutating the effects chain from outside it) and refuses politely rather than dereferencing a nil streamer when nothing has been loaded yet.
145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
import { EventsOn } from '@runtime/runtime';
|
|
import { Events } from '../events';
|
|
import * as Player from '@go/player/Player';
|
|
|
|
// TrackInfo mirrors the player.TrackInfo struct in the Go backend.
|
|
// Fields are serialized as camelCase JSON via struct tags.
|
|
export interface TrackInfo {
|
|
fileName: string;
|
|
filePath: string;
|
|
trackLength: number; // in seconds
|
|
seekPosition: number; // current playback position in seconds
|
|
state: string; // playback state from backend
|
|
title: string; // track title (falls back to fileName)
|
|
artist: string; // artist name
|
|
album: string; // album name
|
|
coverArt: string; // URL path to full-size cover art or empty string
|
|
coverArtSmall: string; // URL path to small variant (100px max) or empty string
|
|
coverArtMedium: string; // URL path to medium variant (200px max) or empty string
|
|
coverArtLarge: string; // URL path to large variant (400px max) or empty string
|
|
trackChangeId: number; // monotonic counter to detect track changes even when the same file plays consecutively
|
|
artistMbid: string; // MusicBrainz artist ID or empty string
|
|
releaseGroupMbid: string; // MusicBrainz release group ID or empty string
|
|
recordingMbid: string; // MusicBrainz recording ID or empty string
|
|
}
|
|
|
|
export interface PlayerState {
|
|
// Cached from backend
|
|
isPlaying: boolean;
|
|
currentTrack: TrackInfo | null;
|
|
volume: number; // 0-100
|
|
muted: boolean; // silenced independently of the volume level
|
|
|
|
// Frontend-only state (for future use)
|
|
// selectedTrackIds: Set<number>;
|
|
// isQueuePanelOpen: boolean;
|
|
}
|
|
|
|
type Subscriber = () => void;
|
|
|
|
class PlayerStore {
|
|
private state: PlayerState = {
|
|
isPlaying: false,
|
|
currentTrack: null,
|
|
volume: 50,
|
|
muted: false,
|
|
};
|
|
|
|
private subscribers = new Set<Subscriber>();
|
|
private notifyScheduled = false;
|
|
|
|
constructor() {
|
|
this.initializeEventListeners();
|
|
}
|
|
|
|
// ===================================================================
|
|
// WAILS EVENT BRIDGE
|
|
// Subscribe to backend events and update cached state
|
|
// ===================================================================
|
|
|
|
private initializeEventListeners(): void {
|
|
EventsOn(Events.PlaybackStateChanged, (data: { state: string }) => {
|
|
this.update({ isPlaying: data.state === 'playing' });
|
|
});
|
|
|
|
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo | null) => {
|
|
this.update({ currentTrack: trackInfo ?? null });
|
|
});
|
|
|
|
EventsOn(Events.PlaybackFinished, () => {
|
|
this.update({ isPlaying: false });
|
|
// Queue auto-advance is handled by the backend queue package.
|
|
});
|
|
|
|
EventsOn(Events.VolumeChanged, (volume: number) => {
|
|
this.update({ volume });
|
|
});
|
|
|
|
EventsOn(Events.MuteChanged, (muted: boolean) => {
|
|
this.update({ muted });
|
|
});
|
|
}
|
|
|
|
// ===================================================================
|
|
// STATE ACCESS
|
|
// ===================================================================
|
|
|
|
getState(): Readonly<PlayerState> {
|
|
return this.state;
|
|
}
|
|
|
|
// ===================================================================
|
|
// ACTIONS
|
|
// These delegate to the backend via Wails bindings
|
|
// ===================================================================
|
|
|
|
pause(): void {
|
|
Player.Pause();
|
|
}
|
|
|
|
loadTrack(filePath: string): void {
|
|
Player.LoadFile(filePath);
|
|
}
|
|
|
|
seek(seconds: number): void {
|
|
Player.Seek(seconds);
|
|
}
|
|
|
|
setVolume(level: number): void {
|
|
Player.SetVolume(level);
|
|
}
|
|
|
|
toggleMute(): void {
|
|
void Player.MuteToggle();
|
|
}
|
|
|
|
// ===================================================================
|
|
// SUBSCRIPTION SYSTEM
|
|
// ===================================================================
|
|
|
|
subscribe(callback: Subscriber): () => void {
|
|
this.subscribers.add(callback);
|
|
|
|
return () => this.subscribers.delete(callback);
|
|
}
|
|
|
|
private update(partial: Partial<PlayerState>): void {
|
|
this.state = { ...this.state, ...partial };
|
|
this.notify();
|
|
}
|
|
|
|
private notify(): void {
|
|
if (this.notifyScheduled) return;
|
|
this.notifyScheduled = true;
|
|
queueMicrotask(() => {
|
|
this.notifyScheduled = false;
|
|
for (const sub of this.subscribers) {
|
|
sub();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
export const playerStore = new PlayerStore();
|