@@ -199,11 +241,32 @@ export class NowPlaying extends LitElement {
: nothing}
{
if (this.activePlaylistIndex < 0) return;
@@ -2134,6 +2156,18 @@ export class PlaylistView
▶
+
+ this.onContextMenuFavoriteToggle()}
+ @mouseenter=${() =>
+ this.ctxMenu.closePlaylistSubmenu()}
+ >
+
+ ${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
+
${this.selection
.selectionCount ===
1
diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts
index 0dfa09f..c079ad9 100644
--- a/frontend/src/components/queue-panel/queue-panel.ts
+++ b/frontend/src/components/queue-panel/queue-panel.ts
@@ -23,6 +23,7 @@ import {
contextMenuStyles,
} from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
+import { FavoritesController } from '@store/controllers/favorites-controller';
import {
hasTrackPayload,
getDragPayload,
@@ -52,6 +53,7 @@ export class QueuePanel
private queue = new QueueController(this);
private selection = new SelectionController(this);
private ctxMenu = new ContextMenuController(this);
+ private favCtrl = new FavoritesController(this);
@property({ type: Boolean, reflect: true })
open = false;
@@ -622,6 +624,26 @@ export class QueuePanel
this.ctxMenu.close();
}
+ private onContextMenuFavoriteToggle() {
+ const filePaths =
+ this.getSelectedFilePaths();
+
+ if (filePaths.length === 0) return;
+
+ if (this.favCtrl.allFavorited(filePaths)) {
+ void this.favCtrl.removeFromFavorites(
+ filePaths,
+ );
+ } else {
+ void this.favCtrl.addToFavorites(
+ filePaths,
+ );
+ }
+
+ this.selection.clear();
+ this.ctxMenu.close();
+ }
+
private openTrackDetails(index: number) {
const queueTrack =
this.queue.tracks[index];
@@ -1330,6 +1352,18 @@ export class QueuePanel
▶
+
+ this.onContextMenuFavoriteToggle()}
+ @mouseenter=${() =>
+ this.ctxMenu.closePlaylistSubmenu()}
+ >
+
+ ${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`}
+
${this.selection
.selectionCount === 1
? html`
diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts
index c498644..60c343e 100644
--- a/frontend/src/components/track-list/track-list.ts
+++ b/frontend/src/components/track-list/track-list.ts
@@ -17,6 +17,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js';
import { PlayerController } from '@store/controllers/player-controller';
import { SearchController } from '@store/controllers/search-controller';
import { TrackListController } from '@store/controllers/tracklist-controller';
+import { FavoritesController } from '@store/controllers/favorites-controller';
import { queueStore } from '@store/queue-store';
import { LibraryController } from '@store/controllers/library-controller';
import {
@@ -74,6 +75,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
private libraryCtrl = new LibraryController(this);
private searchCtrl = new SearchController(this);
private trackListCtrl = new TrackListController(this);
+ private favCtrl = new FavoritesController(this);
private selection = new SelectionController(this);
private ctxMenu = new ContextMenuController(this);
private lastSearchTerm = '';
@@ -312,24 +314,34 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
private get gridTemplateColumns(): string {
const cols = this.activeColumns;
+ const favCol = '24px';
if (this.columnWidths.length === 0) {
- return cols
- .map((c) => c.defaultWidth)
- .join(' ');
+ return (
+ favCol +
+ ' ' +
+ cols
+ .map((c) => c.defaultWidth)
+ .join(' ')
+ );
}
- return this.columnWidths
- .map((w) => `${w}px`)
- .join(' ');
+ return (
+ favCol +
+ ' ' +
+ this.columnWidths
+ .map((w) => `${w}px`)
+ .join(' ')
+ );
}
private get colBoundaryPositions(): number[] {
if (this.columnWidths.length === 0) return [];
const padding = 8;
+ const favColWidth = 24;
const positions: number[] = [];
- let cumulative = padding;
+ let cumulative = padding + favColWidth;
for (
let i = 0;
@@ -930,7 +942,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
min-width: 0;
}
- .header-cell + .header-cell,
+ .header-row > :not(:first-child),
.track-row > :not(:first-child) {
padding-left: 6px;
}
@@ -971,6 +983,31 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
text-align: center;
}
+ .fav-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ flex-shrink: 0;
+ cursor: pointer;
+ color: var(--yj-text-tertiary, #666);
+ font-size: 12px;
+ transition: color 0.1s ease;
+ }
+
+ .fav-icon:hover {
+ color: var(--yj-text-primary, #fff);
+ }
+
+ .fav-icon.favorited {
+ color: var(--yj-accent, #ffd43b);
+ }
+
+ .fav-icon.favorited:hover {
+ color: var(--yj-accent, #ffd43b);
+ opacity: 0.8;
+ }
+
.search-match {
background-color: rgba(255, 212, 59, 0.15);
border-radius: 2px;
@@ -1258,6 +1295,26 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
this.ctxMenu.close();
}
+ private onContextMenuFavoriteToggle() {
+ const filePaths =
+ this.selection.getSelectedKeysOrdered();
+
+ if (filePaths.length === 0) return;
+
+ if (this.favCtrl.allFavorited(filePaths)) {
+ void this.favCtrl.removeFromFavorites(
+ filePaths,
+ );
+ } else {
+ void this.favCtrl.addToFavorites(
+ filePaths,
+ );
+ }
+
+ this.selection.clear();
+ this.ctxMenu.close();
+ }
+
private openTrackDetails(filePath: string) {
const track = this.tracks.find(
(t) => t.FilePath === filePath,
@@ -1479,6 +1536,13 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
const cols = this.activeColumns;
+ const isFav = this.favCtrl.isFavorited(
+ track.FilePath,
+ );
+ const favVariant = isFav
+ ? 'solid'
+ : 'regular';
+
return html`
+
{
+ e.stopPropagation();
+ void this.favCtrl.toggleFavorite(
+ track.FilePath,
+ );
+ }}
+ >
+
+
${cols.map((col) => {
const val = col.accessor(track);
const centered = val === '\u2014';
@@ -1628,6 +1706,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
${this.renderSortToolbar()}