Files
yellowjacket/frontend/src/store/favorites-store.ts
T
yonlu e6378e1f0d feat(quick-7): wire frontend pin-default-playlist feature end-to-end
- Add pinDefault state, getter, setter to favorites-store
- Add pinDefault getter and setPinDefault to favorites-controller
- Update sortedEntries in playlist-view to pin default playlist to top
- Add Pin to Top toggle in config page Favorites section
- Add Wails bindings for GetPinDefaultPlaylist/SetPinDefaultPlaylist
- React to PinDefault in FavoritesConfigChanged event payload
2026-03-01 10:06:56 -05:00

300 lines
7.5 KiB
TypeScript

import { EventsOn } from '@runtime/runtime';
import {
GetDefaultPlaylistTrackPaths,
GetDefaultPlaylistInfo,
ToggleDefaultPlaylistTrack,
AddToDefaultPlaylist,
RemoveFromDefaultPlaylist,
} from '@go/playlist/Service';
import {
GetFavoritesIconStyle,
GetFavoritesPlaylistID,
GetPinDefaultPlaylist,
SetFavoritesIconStyle,
SetFavoritesPlaylistID,
SetPinDefaultPlaylist,
} from '@go/config/Config';
import { Events } from '../events';
export type IconStyle = 'heart' | 'star';
export interface FavoritesState {
playlistId: number;
playlistName: string;
iconStyle: IconStyle;
favoritedPaths: Set<string>;
}
type Subscriber = () => void;
class FavoritesStore {
private playlistId = 0;
private playlistName = 'Favorites';
private iconStyle: IconStyle = 'heart';
private pinDefault = true;
private favoritedPaths = new Set<string>();
private subscribers = new Set<Subscriber>();
private loading = false;
constructor() {
// Load initial state.
void this.loadConfig();
void this.loadPaths();
// React to changes from the backend.
EventsOn(
Events.FavoritesConfigChanged,
(data: {
PlaylistID: number;
IconStyle: string;
PinDefault: boolean;
}) => {
this.playlistId = data.PlaylistID;
this.iconStyle =
data.IconStyle as IconStyle;
this.pinDefault = data.PinDefault;
this.notify();
void this.loadPlaylistName();
void this.loadPaths();
},
);
EventsOn(
Events.DefaultPlaylistChanged,
() => {
void this.loadPaths();
},
);
// When a playlist's tracks change, check if it's
// our default playlist and reload if so.
EventsOn(
Events.PlaylistTracksChanged,
(playlistId: number) => {
if (playlistId === this.playlistId) {
void this.loadPaths();
}
},
);
// When a playlist is deleted and recreated,
// reload everything.
EventsOn(Events.PlaylistDeleted, () => {
void this.loadConfig();
void this.loadPaths();
});
EventsOn(Events.PlaylistRenamed, () => {
void this.loadPlaylistName();
});
EventsOn(Events.PlaylistsRestored, () => {
void this.loadPaths();
});
}
// ===============================================================
// DATA ACCESS
// ===============================================================
isFavorited(filePath: string): boolean {
return this.favoritedPaths.has(filePath);
}
/**
* Check if all given file paths are in the default
* playlist.
*/
allFavorited(filePaths: string[]): boolean {
if (filePaths.length === 0) return false;
return filePaths.every((fp) =>
this.favoritedPaths.has(fp),
);
}
getIconStyle(): IconStyle {
return this.iconStyle;
}
getPlaylistName(): string {
return this.playlistName;
}
getPlaylistId(): number {
return this.playlistId;
}
getPinDefault(): boolean {
return this.pinDefault;
}
isLoading(): boolean {
return this.loading;
}
// ===============================================================
// ACTIONS
// ===============================================================
async toggleFavorite(filePath: string): Promise<void> {
// Optimistic update.
const wasIn = this.favoritedPaths.has(filePath);
if (wasIn) {
this.favoritedPaths.delete(filePath);
} else {
this.favoritedPaths.add(filePath);
}
this.notify();
try {
await ToggleDefaultPlaylistTrack(filePath);
} catch {
// Revert optimistic update.
if (wasIn) {
this.favoritedPaths.add(filePath);
} else {
this.favoritedPaths.delete(filePath);
}
this.notify();
}
}
async addToFavorites(
filePaths: string[],
): Promise<void> {
for (const fp of filePaths) {
this.favoritedPaths.add(fp);
}
this.notify();
try {
await AddToDefaultPlaylist(filePaths);
} catch {
void this.loadPaths();
}
}
async removeFromFavorites(
filePaths: string[],
): Promise<void> {
for (const fp of filePaths) {
this.favoritedPaths.delete(fp);
}
this.notify();
try {
await RemoveFromDefaultPlaylist(filePaths);
} catch {
void this.loadPaths();
}
}
async setIconStyle(
style: IconStyle,
): Promise<void> {
this.iconStyle = style;
this.notify();
await SetFavoritesIconStyle(style);
}
async setDefaultPlaylist(
id: number,
): Promise<void> {
this.playlistId = id;
this.notify();
await SetFavoritesPlaylistID(id);
await this.loadPlaylistName();
await this.loadPaths();
}
async setPinDefault(
pin: boolean,
): Promise<void> {
this.pinDefault = pin;
this.notify();
await SetPinDefaultPlaylist(pin);
}
// ===============================================================
// SUBSCRIPTION SYSTEM
// ===============================================================
subscribe(callback: Subscriber): () => void {
this.subscribers.add(callback);
return () => this.subscribers.delete(callback);
}
private notify(): void {
this.subscribers.forEach((cb) => cb());
}
// ===============================================================
// LOADING HELPERS
// ===============================================================
private async loadConfig(): Promise<void> {
try {
const [id, style, pin] =
await Promise.all([
GetFavoritesPlaylistID(),
GetFavoritesIconStyle(),
GetPinDefaultPlaylist(),
]);
this.playlistId = id;
this.iconStyle = style as IconStyle;
this.pinDefault = pin;
await this.loadPlaylistName();
this.notify();
} catch {
// Defaults are already set.
}
}
private async loadPlaylistName(): Promise<void> {
if (this.playlistId === 0) {
this.playlistName = 'Favorites';
this.notify();
return;
}
try {
const info =
await GetDefaultPlaylistInfo();
if (info?.Name) {
this.playlistName = info.Name;
this.notify();
}
} catch {
// Keep current name.
}
}
private async loadPaths(): Promise<void> {
this.loading = true;
try {
const paths =
await GetDefaultPlaylistTrackPaths();
this.favoritedPaths = new Set(paths ?? []);
} catch {
// Keep current set.
} finally {
this.loading = false;
this.notify();
}
}
}
// Singleton instance.
export const favoritesStore = new FavoritesStore();