Files
music/web/MusicHeart.tsx
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

44 lines
1.5 KiB
TypeScript

import { Heart } from 'lucide-react';
import type { FavoriteKind } from './shared';
import { useMusicFavorites } from './useMusicFavorites';
/**
* A heart toggle for a favoritable thing (track / album / artist). Reads and writes the shared
* favorites cache, so every heart for the same key stays in sync and flips optimistically. Stops click
* propagation so it works inside clickable rows/cards. Renders nothing without a key.
*/
export const MusicHeart = ({
kind,
favKey,
size = 18,
className = '',
hoverReveal = false,
}: {
kind: FavoriteKind;
favKey: string;
size?: number;
className?: string;
/** When set, a NOT-favorited heart is hidden until the enclosing `group` is hovered/focused; a
* favorited (filled) heart always stays visible. Keeps dense lists uncluttered. */
hoverReveal?: boolean;
}) => {
const { isFavorite, toggle } = useMusicFavorites();
if (!favKey) return null;
const on = isFavorite(kind, favKey);
const reveal = hoverReveal && !on ? 'opacity-0 group-hover:opacity-100 focus:opacity-100' : '';
return (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggle(kind, favKey);
}}
title={on ? 'Remove from favorites' : 'Add to favorites'}
aria-label={on ? 'Remove from favorites' : 'Add to favorites'}
className={`flex cursor-pointer items-center justify-center transition-colors ${reveal} ${className}`}
>
<Heart size={size} className={on ? 'fill-red-500 text-red-500' : 'text-muted-foreground hover:text-foreground'} />
</button>
);
};