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:
@@ -174,8 +174,24 @@ export class YjCombobox extends LitElement {
|
||||
// fire first.
|
||||
requestAnimationFrame(() => {
|
||||
this.open = false;
|
||||
// Restore display text to the confirmed value.
|
||||
this.filterText = this.value;
|
||||
|
||||
// Commit free-form text: if the user typed something that
|
||||
// isn't in the option list, accept it as the value anyway.
|
||||
const typed = this.filterText.trim();
|
||||
if (typed && typed !== this.value) {
|
||||
this.value = typed;
|
||||
this.filterText = typed;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('combobox-change', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { value: typed },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Restore display text to the confirmed value.
|
||||
this.filterText = this.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,6 +227,11 @@ export class YjCombobox extends LitElement {
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.selectOption(opts[this.highlightedIndex]!);
|
||||
} else if (this.filterText.trim()) {
|
||||
// Commit free-form text on Enter even without a
|
||||
// highlighted option.
|
||||
e.preventDefault();
|
||||
this.selectOption(this.filterText.trim());
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -261,8 +261,11 @@ export class SmartPlaylistDetails extends LitElement {
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
padding: 0 20px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
`];
|
||||
|
||||
@@ -270,13 +273,17 @@ export class SmartPlaylistDetails extends LitElement {
|
||||
// Lifecycle
|
||||
// =================================================================
|
||||
|
||||
override async connectedCallback() {
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadTracks();
|
||||
|
||||
if (this.autoEdit) {
|
||||
// Skip evaluation for new playlists — go straight to editor.
|
||||
this.autoEdit = false;
|
||||
this.loading = false;
|
||||
this.tracks = [];
|
||||
this.handleEditRules();
|
||||
} else {
|
||||
void this.loadTracks();
|
||||
}
|
||||
|
||||
this.playlistDeletedCleanup = EventsOn(
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user