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:
Frank Stellmacher
2026-04-22 21:14:13 +02:00
committed by GitHub
parent b4f31e0954
commit db72ed9e5a
11 changed files with 574 additions and 45 deletions
+7
View File
@@ -117,6 +117,13 @@ export const deTranslation = {
rgAutoTooltip: 'ReplayGain (Auto)',
showMoreTracks: '{{count}} weitere anzeigen',
showLessTracks: 'Weniger anzeigen',
hideCard: 'Karte ausblenden',
layoutMenu: 'Layout',
visibleCards: 'Sichtbare Karten',
hiddenCards: 'Ausgeblendete Karten',
noHiddenCards: 'Keine ausgeblendeten Karten',
resetLayout: 'Layout zurücksetzen',
emptyColumn: 'Karten hier ablegen',
},
contextMenu: {
playNow: 'Direkt abspielen',
+7
View File
@@ -118,6 +118,13 @@ export const enTranslation = {
rgAutoTooltip: 'ReplayGain (auto)',
showMoreTracks: 'Show {{count}} more',
showLessTracks: 'Show less',
hideCard: 'Hide card',
layoutMenu: 'Layout',
visibleCards: 'Visible cards',
hiddenCards: 'Hidden cards',
noHiddenCards: 'No hidden cards',
resetLayout: 'Reset layout',
emptyColumn: 'Drop cards here',
},
contextMenu: {
playNow: 'Play Now',
+7
View File
@@ -118,6 +118,13 @@ export const esTranslation = {
rgAutoTooltip: 'ReplayGain (auto)',
showMoreTracks: 'Mostrar {{count}} más',
showLessTracks: 'Mostrar menos',
hideCard: 'Ocultar tarjeta',
layoutMenu: 'Diseño',
visibleCards: 'Tarjetas visibles',
hiddenCards: 'Tarjetas ocultas',
noHiddenCards: 'No hay tarjetas ocultas',
resetLayout: 'Restablecer diseño',
emptyColumn: 'Suelta tarjetas aquí',
},
contextMenu: {
playNow: 'Reproducir Ahora',
+7
View File
@@ -117,6 +117,13 @@ export const frTranslation = {
rgAutoTooltip: 'ReplayGain (auto)',
showMoreTracks: 'Afficher {{count}} de plus',
showLessTracks: 'Réduire',
hideCard: 'Masquer la carte',
layoutMenu: 'Disposition',
visibleCards: 'Cartes visibles',
hiddenCards: 'Cartes masquées',
noHiddenCards: 'Aucune carte masquée',
resetLayout: 'Réinitialiser la disposition',
emptyColumn: 'Déposez des cartes ici',
},
contextMenu: {
playNow: 'Lire maintenant',
+7
View File
@@ -117,6 +117,13 @@ export const nbTranslation = {
rgAutoTooltip: 'ReplayGain (auto)',
showMoreTracks: 'Vis {{count}} til',
showLessTracks: 'Vis mindre',
hideCard: 'Skjul kort',
layoutMenu: 'Oppsett',
visibleCards: 'Synlige kort',
hiddenCards: 'Skjulte kort',
noHiddenCards: 'Ingen skjulte kort',
resetLayout: 'Tilbakestill oppsett',
emptyColumn: 'Slipp kort her',
},
contextMenu: {
playNow: 'Spill nå',
+7
View File
@@ -117,6 +117,13 @@ export const nlTranslation = {
rgAutoTooltip: 'ReplayGain (auto)',
showMoreTracks: '{{count}} meer tonen',
showLessTracks: 'Minder tonen',
hideCard: 'Kaart verbergen',
layoutMenu: 'Lay-out',
visibleCards: 'Zichtbare kaarten',
hiddenCards: 'Verborgen kaarten',
noHiddenCards: 'Geen verborgen kaarten',
resetLayout: 'Lay-out resetten',
emptyColumn: 'Sleep kaarten hier',
},
contextMenu: {
playNow: 'Nu afspelen',
+7
View File
@@ -122,6 +122,13 @@ export const ruTranslation = {
rgAutoTooltip: 'ReplayGain (авто)',
showMoreTracks: 'Показать ещё {{count}}',
showLessTracks: 'Свернуть',
hideCard: 'Скрыть карточку',
layoutMenu: 'Макет',
visibleCards: 'Видимые карточки',
hiddenCards: 'Скрытые карточки',
noHiddenCards: 'Нет скрытых карточек',
resetLayout: 'Сбросить макет',
emptyColumn: 'Перетащите карточки сюда',
},
contextMenu: {
playNow: 'Играть сейчас',
+7
View File
@@ -117,6 +117,13 @@ export const zhTranslation = {
rgAutoTooltip: 'ReplayGain (自动)',
showMoreTracks: '显示另外 {{count}} 首',
showLessTracks: '收起',
hideCard: '隐藏卡片',
layoutMenu: '布局',
visibleCards: '可见卡片',
hiddenCards: '已隐藏的卡片',
noHiddenCards: '没有已隐藏的卡片',
resetLayout: '重置布局',
emptyColumn: '将卡片拖到此处',
},
contextMenu: {
playNow: '立即播放',