Files
yellowjacket/frontend/src/store/player-store.ts
T

124 lines
3.6 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
}
export interface PlayerState {
// Cached from backend
isPlaying: boolean;
currentTrack: TrackInfo | null;
volume: number; // 0-100
// 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,
};
private subscribers = new Set<Subscriber>();
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 });
});
}
// ===================================================================
// 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);
}
// ===================================================================
// 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 {
this.subscribers.forEach((callback) => callback());
}
}
// Singleton instance
export const playerStore = new PlayerStore();