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>
44 lines
1.5 KiB
TypeScript
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>
|
|
);
|
|
};
|