Everything the plugin is, moved out of officerdev/platform on 2026-08-15 — 41 files, unchanged from the tree they left. manifest.ts identity, one permission, ffmpeg/ffprobe declared api/ the sidecar proxy; the prefix comes from mountPrefix() sidecar/ the whole /api/music contract — indexing, streaming, per-user state db/ music_favorites, _playlists, _playlist_items, _now_playing web/ panels, layout, and the player: engine, bar, lyrics, favourites cliamp/ the second playback path, parked — not working, kept deliberately widgets/ the dashboard widget, parked — plugins cannot contribute widgets assets/ icon.png, the dock tile scripts/ the reindex CLI PLUGIN.md is the design record: what moved, what stayed, what broke, and why. MUSIC_API.md is the contract the phone and tablet apps speak, and the reason the sidecar's HTTP shape is not free to change. ── It does not build here, and that is the point ── The platform resolves `hooks/useClient`, `officerdev`, `officerdb/db` and `@@/*` through the workspace links in its own node_modules. Measured from this directory, outside the platform checkout, every one of them fails to resolve — 7 imports in the backend, ~29 in the frontend. So this repository is the source of truth, not yet a buildable unit. Making it one means the host API becoming something a plugin can depend on rather than something it reaches into. That is the next problem, and having the code here is what makes it unavoidable rather than theoretical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useClient } from 'hooks/useClient';
|
|
import type { FavoriteKind, GroupedFavorites } from './shared';
|
|
|
|
const KEY = ['music', 'favorites'] as const;
|
|
const EMPTY: GroupedFavorites = { tracks: [], albums: [], artists: [] };
|
|
const groupOf = (kind: FavoriteKind): keyof GroupedFavorites =>
|
|
kind === 'track' ? 'tracks' : kind === 'album' ? 'albums' : 'artists';
|
|
|
|
/**
|
|
* The user's music favorites (tracks / albums / artists) for the /music workspace, backed by the
|
|
* platform's `/api/music/favorites`. One shared react-query cache, so every heart reflects the same
|
|
* state; toggling is optimistic (flips instantly, rolls back on failure).
|
|
*/
|
|
export function useMusicFavorites() {
|
|
const { get, post, delete: del } = useClient();
|
|
const qc = useQueryClient();
|
|
|
|
const { data } = useQuery({
|
|
queryKey: KEY,
|
|
queryFn: () => get<GroupedFavorites>('/music/favorites'),
|
|
staleTime: 60_000,
|
|
});
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ on, kind, key }: { on: boolean; kind: FavoriteKind; key: string }) =>
|
|
on
|
|
? post('/music/favorites', { kind, key })
|
|
: del(`/music/favorites?kind=${encodeURIComponent(kind)}&key=${encodeURIComponent(key)}`),
|
|
onMutate: async ({ on, kind, key }) => {
|
|
await qc.cancelQueries({ queryKey: KEY });
|
|
const prev = qc.getQueryData<GroupedFavorites>(KEY) ?? EMPTY;
|
|
const g = groupOf(kind);
|
|
qc.setQueryData<GroupedFavorites>(KEY, {
|
|
...prev,
|
|
[g]: on ? [key, ...prev[g].filter((k) => k !== key)] : prev[g].filter((k) => k !== key),
|
|
});
|
|
return { prev };
|
|
},
|
|
onError: (_e, _v, ctx) => {
|
|
if (ctx?.prev) qc.setQueryData(KEY, ctx.prev);
|
|
},
|
|
});
|
|
|
|
const isFavorite = (kind: FavoriteKind, key: string) => (data ?? EMPTY)[groupOf(kind)].includes(key);
|
|
const toggle = (kind: FavoriteKind, key: string) => {
|
|
if (!key) return;
|
|
mutation.mutate({ on: !isFavorite(kind, key), kind, key });
|
|
};
|
|
|
|
return { favorites: data ?? EMPTY, isFavorite, toggle };
|
|
}
|