feat(M002): smart playlists — rule engine, editor UI, sidebar integration

Recovered from orphaned worktree commits (complete-milestone failed to merge).

Backend:
- Migration 9: is_smart + smart_rules columns on playlists table
- smartplaylist package: parameterized WHERE clause builder, field whitelist, genre subquery
- playlist.Service: Create/Update/Evaluate/Preview/GetRules smart playlist methods
- 65 tests (49 rule engine + 15 service + 1 migration)

Frontend:
- yj-combobox: reusable typeable dropdown with keyboard nav, ARIA, blur-race fix
- smart-playlist-editor: row-based rule builder with live preview
- smart-playlist-details: evaluate, refresh, play, shuffle, edit rules
- Sidebar: filter icon, Smart badge, create button, routing
- Queue snapshot on play/shuffle
This commit is contained in:
2026-03-21 12:53:00 -04:00
parent e974bd2a22
commit 477b7ff6a2
17 changed files with 5342 additions and 26 deletions
+2
View File
@@ -483,6 +483,7 @@ export namespace playlist {
Name: string;
CreatedAt: string;
UpdatedAt: string;
IsSmart: boolean;
static createFrom(source: any = {}) {
return new Summary(source);
@@ -494,6 +495,7 @@ export namespace playlist {
this.Name = source["Name"];
this.CreatedAt = source["CreatedAt"];
this.UpdatedAt = source["UpdatedAt"];
this.IsSmart = source["IsSmart"];
}
}
export class Track {
+11
View File
@@ -2,6 +2,7 @@
// This file is automatically generated. DO NOT EDIT
import {playlist} from '../models';
import {context} from '../models';
import {library} from '../models';
export function AddToDefaultPlaylist(arg1:Array<string>):Promise<void>;
@@ -11,10 +12,14 @@ export function CreatePlaylist(arg1:string):Promise<playlist.Summary>;
export function CreatePlaylistWithTracks(arg1:string,arg2:Array<string>):Promise<playlist.Summary>;
export function CreateSmartPlaylist(arg1:string,arg2:string):Promise<playlist.Summary>;
export function DeletePlaylist(arg1:number):Promise<void>;
export function EnsureDefaultPlaylist():Promise<void>;
export function EvaluateSmartPlaylist(arg1:number):Promise<Array<library.Track>>;
export function FindDuplicateTracksInPlaylist(arg1:number,arg2:Array<string>):Promise<playlist.DuplicateCheckResult>;
export function FindPhantomMatches(arg1:number,arg2:Array<string>):Promise<playlist.PhantomSearchResult>;
@@ -56,3 +61,9 @@ export function SetContext(arg1:context.Context):Promise<void>;
export function SetFavoritesConfig(arg1:playlist.FavoritesConfigProvider):Promise<void>;
export function ToggleDefaultPlaylistTrack(arg1:string):Promise<boolean>;
export function UpdateSmartPlaylistRules(arg1:number,arg2:string):Promise<void>;
export function GetSmartPlaylistRules(arg1:number):Promise<string>;
export function PreviewSmartPlaylist(arg1:string):Promise<Array<library.Track>>;
+20
View File
@@ -18,6 +18,10 @@ export function CreatePlaylistWithTracks(arg1, arg2) {
return window['go']['playlist']['Service']['CreatePlaylistWithTracks'](arg1, arg2);
}
export function CreateSmartPlaylist(arg1, arg2) {
return window['go']['playlist']['Service']['CreateSmartPlaylist'](arg1, arg2);
}
export function DeletePlaylist(arg1) {
return window['go']['playlist']['Service']['DeletePlaylist'](arg1);
}
@@ -26,6 +30,10 @@ export function EnsureDefaultPlaylist() {
return window['go']['playlist']['Service']['EnsureDefaultPlaylist']();
}
export function EvaluateSmartPlaylist(arg1) {
return window['go']['playlist']['Service']['EvaluateSmartPlaylist'](arg1);
}
export function FindDuplicateTracksInPlaylist(arg1, arg2) {
return window['go']['playlist']['Service']['FindDuplicateTracksInPlaylist'](arg1, arg2);
}
@@ -109,3 +117,15 @@ export function SetFavoritesConfig(arg1) {
export function ToggleDefaultPlaylistTrack(arg1) {
return window['go']['playlist']['Service']['ToggleDefaultPlaylistTrack'](arg1);
}
export function UpdateSmartPlaylistRules(arg1, arg2) {
return window['go']['playlist']['Service']['UpdateSmartPlaylistRules'](arg1, arg2);
}
export function GetSmartPlaylistRules(arg1) {
return window['go']['playlist']['Service']['GetSmartPlaylistRules'](arg1);
}
export function PreviewSmartPlaylist(arg1) {
return window['go']['playlist']['Service']['PreviewSmartPlaylist'](arg1);
}