Files
music/web/useMusicFavorites.ts
T
Claude Opus 5 6e07da7a36 music, extracted from the platform into its own repository
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>
2026-08-15 17:34:51 +00:00

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 };
}