mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(now-playing): draggable widget cards with per-user layout (#267)
Each dashboard card (Album, Top Songs, Credits, Artist, Discography, On Tour) is now a widget the user grabs and drops anywhere — reorder within a column or move across columns. Layout persists per-install via the new nowPlayingLayoutStore (Zustand + localStorage, same shape as sidebarStore with an onRehydrateStorage guard for unknown IDs). Drag uses psyDnD (useDragSource / psy-drop event, the mouse-event based system from DragDropContext) — HTML5 native DnD is a no-go on WebKitGTK Linux where it always shows a forbidden cursor. The whole card is the drag handle; the column listens via a document-level mousemove for the drop indicator and via a global psy-drop listener that reads the latest hovered column from a ref, so drops below the last card still land correctly regardless of column height. Visibility is toggled from a layout menu in the top-right of the dashboard — two sections (visible / hidden) plus a reset-to-defaults button. No hide buttons on the cards themselves (keeps the card UI uncluttered). An equal-height grid (align-items: stretch + min-height on columns) keeps the drop zone active for the full vertical span of either column. Co-authored-by: Psychotoxical <dev@psysonic.app> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
b4f31e0954
commit
db72ed9e5a
@@ -0,0 +1,76 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export type NpColumn = 'left' | 'right';
|
||||
|
||||
export type NpCardId =
|
||||
| 'album'
|
||||
| 'topSongs'
|
||||
| 'credits'
|
||||
| 'artist'
|
||||
| 'discography'
|
||||
| 'tour';
|
||||
|
||||
export interface NpCardConfig {
|
||||
id: NpCardId;
|
||||
column: NpColumn;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export const NP_CARD_IDS: NpCardId[] = ['album', 'topSongs', 'credits', 'artist', 'discography', 'tour'];
|
||||
|
||||
export const DEFAULT_NP_LAYOUT: NpCardConfig[] = [
|
||||
{ id: 'album', column: 'left', visible: true },
|
||||
{ id: 'topSongs', column: 'left', visible: true },
|
||||
{ id: 'credits', column: 'left', visible: true },
|
||||
{ id: 'artist', column: 'right', visible: true },
|
||||
{ id: 'discography', column: 'right', visible: true },
|
||||
{ id: 'tour', column: 'right', visible: true },
|
||||
];
|
||||
|
||||
interface NpLayoutStore {
|
||||
cards: NpCardConfig[];
|
||||
/** Move a card to a column at a given insertion index (0-based within that column). */
|
||||
moveCard: (id: NpCardId, toColumn: NpColumn, toIndex: number) => void;
|
||||
setVisible: (id: NpCardId, visible: boolean) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useNpLayoutStore = create<NpLayoutStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
cards: DEFAULT_NP_LAYOUT,
|
||||
|
||||
moveCard: (id, toColumn, toIndex) => set((s) => {
|
||||
const target = s.cards.find(c => c.id === id);
|
||||
if (!target) return s;
|
||||
const without = s.cards.filter(c => c.id !== id);
|
||||
const left = without.filter(c => c.column === 'left');
|
||||
const right = without.filter(c => c.column === 'right');
|
||||
const moved: NpCardConfig = { ...target, column: toColumn };
|
||||
const targetBucket = toColumn === 'left' ? left : right;
|
||||
const clamped = Math.max(0, Math.min(toIndex, targetBucket.length));
|
||||
targetBucket.splice(clamped, 0, moved);
|
||||
return { cards: [...left, ...right] };
|
||||
}),
|
||||
|
||||
setVisible: (id, visible) => set((s) => ({
|
||||
cards: s.cards.map(c => c.id === id ? { ...c, visible } : c),
|
||||
})),
|
||||
|
||||
reset: () => set({ cards: DEFAULT_NP_LAYOUT }),
|
||||
}),
|
||||
{
|
||||
name: 'psysonic_np_layout',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return;
|
||||
const safe = (state.cards ?? []).filter((c): c is NpCardConfig =>
|
||||
c != null && typeof c.id === 'string' && NP_CARD_IDS.includes(c.id as NpCardId)
|
||||
);
|
||||
const known = new Set(safe.map(c => c.id));
|
||||
const missing = DEFAULT_NP_LAYOUT.filter(c => !known.has(c.id));
|
||||
state.cards = missing.length > 0 ? [...safe, ...missing] : safe;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user