fix: smart playlist UX polish — layout, defaults, free-form input, case-insensitive matching

Details view:
- Skip evaluation on new playlists (was returning all 25k tracks)
- Go straight to editor on auto-edit instead of awaiting loadTracks

Editor:
- Default sort to Random instead of None, remove None option
- Hide sort direction button when sort is Random
- Fixed-width field (160px) and operator (140px) columns, value fills remaining space
- Preview fills remaining vertical space (flex layout) instead of fixed 200px max-height
- Preview scrolls independently while rules/options stay pinned

Combobox:
- Accept free-form text on blur and Enter — no longer requires selection from dropdown
- Enables typing values like 'indie' that may not be exact DB entries

Backend:
- Case-insensitive text matching: COLLATE NOCASE on is/is_not/is_any_of operators
- Applies to both regular fields and genre subqueries
- LIKE operators were already case-insensitive (SQLite default)
This commit is contained in:
2026-03-21 13:25:21 -04:00
parent 477b7ff6a2
commit 6972953649
5 changed files with 69 additions and 33 deletions
@@ -160,7 +160,7 @@ export class SmartPlaylistEditor extends LitElement {
@state() private ruleRows: RuleRow[] = [emptyRule()];
@state() private limit = 0;
@state() private sortField = '';
@state() private sortField = 'random';
@state() private sortDir = '';
@state() private previewTracks: library.Track[] = [];
@state() private previewLoading = false;
@@ -174,7 +174,10 @@ export class SmartPlaylistEditor extends LitElement {
designTokens,
css`
:host {
display: block;
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
}
/* ── Rule rows ────────────────────────── */
@@ -184,17 +187,18 @@ export class SmartPlaylistEditor extends LitElement {
flex-direction: column;
gap: 6px;
padding: 12px 0 8px;
flex-shrink: 0;
}
.rule-row {
display: grid;
grid-template-columns: 1fr 140px 1fr 28px;
grid-template-columns: 160px 140px 1fr 28px;
gap: 6px;
align-items: start;
}
.rule-row.between-row {
grid-template-columns: 1fr 140px 1fr 1fr 28px;
grid-template-columns: 160px 140px 1fr 1fr 28px;
}
/* ── Form controls ────────────────────── */
@@ -289,6 +293,7 @@ export class SmartPlaylistEditor extends LitElement {
var(--yj-border-subtle, rgba(255, 255, 255, 0.06));
margin-top: 4px;
flex-wrap: wrap;
flex-shrink: 0;
}
.option-group {
@@ -337,6 +342,11 @@ export class SmartPlaylistEditor extends LitElement {
var(--yj-border-subtle, rgba(255, 255, 255, 0.06));
margin-top: 8px;
padding-top: 10px;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.preview-header {
@@ -372,10 +382,11 @@ export class SmartPlaylistEditor extends LitElement {
}
.preview-list {
max-height: 200px;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.preview-track {
@@ -467,7 +478,7 @@ export class SmartPlaylistEditor extends LitElement {
this.ruleRows = rows.length > 0 ? rows : [emptyRule()];
this.limit = parsed.limit ?? 0;
this.sortField = parsed.sort_field ?? '';
this.sortField = parsed.sort_field || 'random';
this.sortDir = parsed.sort_dir ?? '';
} catch {
this.ruleRows = [emptyRule()];
@@ -819,9 +830,6 @@ export class SmartPlaylistEditor extends LitElement {
(e.target as HTMLSelectElement).value,
)}
>
<option value="" ?selected=${!this.sortField}>
None
</option>
${SORT_FIELDS.map(
(f) => html`
<option
@@ -833,7 +841,7 @@ export class SmartPlaylistEditor extends LitElement {
`,
)}
</select>
${this.sortField
${this.sortField && this.sortField !== 'random'
? html`
<button
class="sort-dir-btn"