drag and drop enhancements, fixed playlist creation using old db entries, added icons-only sidebar when small width
This commit is contained in:
@@ -52,6 +52,17 @@ func NewDB(logger *slog.Logger) (*DB, error) {
|
|||||||
|
|
||||||
db.SetMaxOpenConns(1) // SQLite only supports one writer at a time
|
db.SetMaxOpenConns(1) // SQLite only supports one writer at a time
|
||||||
|
|
||||||
|
// Enable foreign key enforcement — SQLite disables it by
|
||||||
|
// default, which means ON DELETE CASCADE will not work without
|
||||||
|
// this pragma.
|
||||||
|
if _, err := db.ExecContext(
|
||||||
|
dbCtx, "PRAGMA foreign_keys = ON",
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"could not enable foreign keys: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Execute SQL files from the embedded schemas directory
|
// Execute SQL files from the embedded schemas directory
|
||||||
logger.Debug("reading sql schema files from embedded directory")
|
logger.Debug("reading sql schema files from embedded directory")
|
||||||
|
|
||||||
@@ -86,6 +97,24 @@ func NewDB(logger *slog.Logger) (*DB, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove orphaned playlist_tracks left behind by past deletes
|
||||||
|
// that ran without foreign key enforcement.
|
||||||
|
orphanResult, err := db.ExecContext(
|
||||||
|
dbCtx,
|
||||||
|
"DELETE FROM playlist_tracks WHERE playlist_id NOT IN (SELECT id FROM playlists)",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(
|
||||||
|
"could not clean orphaned playlist tracks",
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
|
} else if n, _ := orphanResult.RowsAffected(); n > 0 {
|
||||||
|
logger.Info(
|
||||||
|
"Cleaned orphaned playlist tracks",
|
||||||
|
"deleted", n,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Get generated queries
|
// Get generated queries
|
||||||
queries := sqlcgen.New(db)
|
queries := sqlcgen.New(db)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreatePlaylist,
|
CreatePlaylist,
|
||||||
|
CreatePlaylistWithTracks,
|
||||||
AddTracksToPlaylist,
|
AddTracksToPlaylist,
|
||||||
RemoveTracksFromPlaylist,
|
RemoveTracksFromPlaylist,
|
||||||
DeletePlaylist,
|
DeletePlaylist,
|
||||||
@@ -166,6 +167,18 @@ export class PlaylistView
|
|||||||
/** Index of the playlist currently hovered during a drag. */
|
/** Index of the playlist currently hovered during a drag. */
|
||||||
@state() private dragOverPlaylistIndex = -1;
|
@state() private dragOverPlaylistIndex = -1;
|
||||||
|
|
||||||
|
/** True when dragging over empty space in the playlist list. */
|
||||||
|
@state() private dragOverEmptyZone = false;
|
||||||
|
|
||||||
|
/** True when dragging over the "New Playlist" button. */
|
||||||
|
@state() private dragOverNewButton = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File paths from a drop that landed outside any playlist.
|
||||||
|
* When non-empty the create form is in "create-and-add" mode.
|
||||||
|
*/
|
||||||
|
private pendingDropPaths: string[] = [];
|
||||||
|
|
||||||
private dragImageEl: HTMLElement | null = null;
|
private dragImageEl: HTMLElement | null = null;
|
||||||
|
|
||||||
@query('#context-menu')
|
@query('#context-menu')
|
||||||
@@ -352,11 +365,19 @@ export class PlaylistView
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-playlist-button:hover {
|
.new-playlist-button:hover,
|
||||||
|
.new-playlist-button.drag-over {
|
||||||
border-color: var(--yj-accent, #ffd43b);
|
border-color: var(--yj-accent, #ffd43b);
|
||||||
color: var(--yj-accent, #ffd43b);
|
color: var(--yj-accent, #ffd43b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-playlist-button.drag-over {
|
||||||
|
background-color: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.15)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.create-form {
|
.create-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -422,6 +443,8 @@ export class PlaylistView
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-item {
|
.playlist-item {
|
||||||
@@ -592,6 +615,58 @@ export class PlaylistView
|
|||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop-zone-icon {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.18)
|
||||||
|
);
|
||||||
|
color: var(--yj-accent, #ffd43b);
|
||||||
|
font-size: 28px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state.drag-over {
|
||||||
|
background-color: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.15)
|
||||||
|
);
|
||||||
|
outline: 2px dashed
|
||||||
|
var(--yj-accent, #ffd43b);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state.drag-over .drop-zone-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.drag-over {
|
||||||
|
background-color: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.15)
|
||||||
|
);
|
||||||
|
outline: 2px dashed
|
||||||
|
var(--yj-accent, #ffd43b);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.drag-over .drop-zone-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
#context-menu {
|
#context-menu {
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
@@ -1095,6 +1170,16 @@ export class PlaylistView
|
|||||||
if (this.dragOverPlaylistIndex !== index) {
|
if (this.dragOverPlaylistIndex !== index) {
|
||||||
this.dragOverPlaylistIndex = index;
|
this.dragOverPlaylistIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A specific playlist is targeted — hide the
|
||||||
|
// "new playlist" drop zone highlights.
|
||||||
|
if (this.dragOverEmptyZone) {
|
||||||
|
this.dragOverEmptyZone = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dragOverNewButton) {
|
||||||
|
this.dragOverNewButton = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPlaylistDragLeave = (
|
private onPlaylistDragLeave = (
|
||||||
@@ -1122,6 +1207,7 @@ export class PlaylistView
|
|||||||
index: number,
|
index: number,
|
||||||
) => {
|
) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
this.dragOverPlaylistIndex = -1;
|
this.dragOverPlaylistIndex = -1;
|
||||||
|
|
||||||
const payload = getDragPayload(e);
|
const payload = getDragPayload(e);
|
||||||
@@ -1161,6 +1247,116 @@ export class PlaylistView
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Drop target (empty space → create new playlist)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
private onEmptyZoneDragOver = (e: DragEvent) => {
|
||||||
|
if (!hasTrackPayload(e)) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show the "new playlist" drop zone when
|
||||||
|
// not hovering a specific playlist item.
|
||||||
|
if (
|
||||||
|
this.dragOverPlaylistIndex === -1 &&
|
||||||
|
!this.dragOverEmptyZone
|
||||||
|
) {
|
||||||
|
this.dragOverEmptyZone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dragOverNewButton) {
|
||||||
|
this.dragOverNewButton = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEmptyZoneDragLeave = (e: DragEvent) => {
|
||||||
|
const related =
|
||||||
|
e.relatedTarget as Node | null;
|
||||||
|
|
||||||
|
if (!related || !this.contains(related)) {
|
||||||
|
this.dragOverEmptyZone = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onEmptyZoneDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.dragOverEmptyZone = false;
|
||||||
|
|
||||||
|
const payload = getDragPayload(e);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!payload ||
|
||||||
|
payload.filePaths.length === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingDropPaths = payload.filePaths;
|
||||||
|
this.creating = true;
|
||||||
|
this.newPlaylistName = '';
|
||||||
|
|
||||||
|
void this.updateComplete.then(() => {
|
||||||
|
const input =
|
||||||
|
this.shadowRoot?.querySelector<HTMLInputElement>(
|
||||||
|
'.create-form input',
|
||||||
|
);
|
||||||
|
|
||||||
|
input?.focus();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Drop target ("New Playlist" button)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
private onNewButtonDragOver = (e: DragEvent) => {
|
||||||
|
if (!hasTrackPayload(e)) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dragOverNewButton) {
|
||||||
|
this.dragOverNewButton = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the empty-zone highlight while
|
||||||
|
// hovering the button.
|
||||||
|
if (this.dragOverEmptyZone) {
|
||||||
|
this.dragOverEmptyZone = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onNewButtonDragLeave = (
|
||||||
|
e: DragEvent,
|
||||||
|
) => {
|
||||||
|
const related =
|
||||||
|
e.relatedTarget as Node | null;
|
||||||
|
const btn =
|
||||||
|
this.shadowRoot?.querySelector(
|
||||||
|
'.new-playlist-button',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (btn && !btn.contains(related)) {
|
||||||
|
this.dragOverNewButton = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onNewButtonDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.dragOverNewButton = false;
|
||||||
|
this.onEmptyZoneDrop(e);
|
||||||
|
};
|
||||||
|
|
||||||
private closeContextMenu(clearSelection = false) {
|
private closeContextMenu(clearSelection = false) {
|
||||||
if (!this.contextMenuOpen) return;
|
if (!this.contextMenuOpen) return;
|
||||||
|
|
||||||
@@ -1430,16 +1626,28 @@ export class PlaylistView
|
|||||||
private handleCancelCreate = () => {
|
private handleCancelCreate = () => {
|
||||||
this.creating = false;
|
this.creating = false;
|
||||||
this.newPlaylistName = '';
|
this.newPlaylistName = '';
|
||||||
|
this.pendingDropPaths = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleCreatePlaylist = async () => {
|
private handleCreatePlaylist = async () => {
|
||||||
const name = this.newPlaylistName.trim();
|
const name = this.newPlaylistName.trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
|
|
||||||
|
const paths = this.pendingDropPaths;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CreatePlaylist(name);
|
if (paths.length > 0) {
|
||||||
|
await CreatePlaylistWithTracks(
|
||||||
|
name,
|
||||||
|
paths,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await CreatePlaylist(name);
|
||||||
|
}
|
||||||
|
|
||||||
this.creating = false;
|
this.creating = false;
|
||||||
this.newPlaylistName = '';
|
this.newPlaylistName = '';
|
||||||
|
this.pendingDropPaths = [];
|
||||||
await this.refreshPlaylists();
|
await this.refreshPlaylists();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -1491,9 +1699,15 @@ export class PlaylistView
|
|||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="new-playlist-button"
|
class="new-playlist-button ${this.dragOverNewButton ? 'drag-over' : ''}"
|
||||||
@click=${this
|
@click=${this
|
||||||
.handleNewPlaylistClick}
|
.handleNewPlaylistClick}
|
||||||
|
@dragover=${this
|
||||||
|
.onNewButtonDragOver}
|
||||||
|
@dragleave=${this
|
||||||
|
.onNewButtonDragLeave}
|
||||||
|
@drop=${this
|
||||||
|
.onNewButtonDrop}
|
||||||
>
|
>
|
||||||
<wa-icon
|
<wa-icon
|
||||||
name="plus"
|
name="plus"
|
||||||
@@ -1693,11 +1907,22 @@ export class PlaylistView
|
|||||||
private renderPlaylistList() {
|
private renderPlaylistList() {
|
||||||
if (this.entries.length === 0) {
|
if (this.entries.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="empty-state">
|
<div
|
||||||
|
class="empty-state ${this.dragOverEmptyZone ? 'drag-over' : ''}"
|
||||||
|
@dragover=${this.onEmptyZoneDragOver}
|
||||||
|
@dragleave=${this.onEmptyZoneDragLeave}
|
||||||
|
@drop=${this.onEmptyZoneDrop}
|
||||||
|
>
|
||||||
|
<div class="drop-zone-icon">
|
||||||
|
<wa-icon
|
||||||
|
name="plus"
|
||||||
|
></wa-icon>
|
||||||
|
</div>
|
||||||
<wa-icon name="list"></wa-icon>
|
<wa-icon name="list"></wa-icon>
|
||||||
<p>No playlists yet</p>
|
<p>No playlists yet</p>
|
||||||
<p style="font-size: 12px;">
|
<p style="font-size: 12px;">
|
||||||
Create a playlist to get started.
|
Create a playlist or drop
|
||||||
|
tracks here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1707,8 +1932,21 @@ export class PlaylistView
|
|||||||
|
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="empty-state">
|
<div
|
||||||
<p>No playlists match your search.</p>
|
class="empty-state ${this.dragOverEmptyZone ? 'drag-over' : ''}"
|
||||||
|
@dragover=${this.onEmptyZoneDragOver}
|
||||||
|
@dragleave=${this.onEmptyZoneDragLeave}
|
||||||
|
@drop=${this.onEmptyZoneDrop}
|
||||||
|
>
|
||||||
|
<div class="drop-zone-icon">
|
||||||
|
<wa-icon
|
||||||
|
name="plus"
|
||||||
|
></wa-icon>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
No playlists match your
|
||||||
|
search.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1727,6 +1965,20 @@ export class PlaylistView
|
|||||||
originalIndex,
|
originalIndex,
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<li
|
||||||
|
class="drop-zone ${this.dragOverEmptyZone ? 'drag-over' : ''}"
|
||||||
|
@dragover=${this
|
||||||
|
.onEmptyZoneDragOver}
|
||||||
|
@dragleave=${this
|
||||||
|
.onEmptyZoneDragLeave}
|
||||||
|
@drop=${this.onEmptyZoneDrop}
|
||||||
|
>
|
||||||
|
<div class="drop-zone-icon">
|
||||||
|
<wa-icon
|
||||||
|
name="plus"
|
||||||
|
></wa-icon>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export class QueuePanel
|
|||||||
private playlistSubmenuOpen = false;
|
private playlistSubmenuOpen = false;
|
||||||
|
|
||||||
private dragOver = false;
|
private dragOver = false;
|
||||||
|
private dragEnterCount = 0;
|
||||||
|
|
||||||
private dropTargetIndex = -1;
|
private dropTargetIndex = -1;
|
||||||
private dropTargetRafId = 0;
|
private dropTargetRafId = 0;
|
||||||
@@ -344,21 +345,6 @@ export class QueuePanel
|
|||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-indicator {
|
|
||||||
display: none;
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--yj-accent, #ffd43b);
|
|
||||||
border-bottom: 1px solid
|
|
||||||
rgba(255, 212, 59, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content.drag-over.empty-drag
|
|
||||||
.drop-indicator {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track-item.drop-before::before {
|
.track-item.drop-before::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -386,18 +372,39 @@ export class QueuePanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 40px 20px;
|
|
||||||
color: var(--yj-text-secondary, #b3b3b3);
|
|
||||||
text-align: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state wa-icon {
|
.drop-zone-icon {
|
||||||
font-size: 32px;
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.18)
|
||||||
|
);
|
||||||
|
color: var(--yj-accent, #ffd43b);
|
||||||
|
font-size: 28px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content.drag-over.empty-drag
|
||||||
|
.empty-state {
|
||||||
|
background-color: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.15)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content.drag-over.empty-drag
|
||||||
|
.drop-zone-icon {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#context-menu {
|
#context-menu {
|
||||||
@@ -761,6 +768,7 @@ export class QueuePanel
|
|||||||
if (!hasTrackPayload(e)) return;
|
if (!hasTrackPayload(e)) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
this.dragEnterCount++;
|
||||||
|
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
const isInternal =
|
const isInternal =
|
||||||
@@ -796,9 +804,16 @@ export class QueuePanel
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onPanelDragLeave = (_e: DragEvent) => {
|
private onPanelDragLeave = (_e: DragEvent) => {
|
||||||
// No-op: cleanup is handled by dragend / drop.
|
this.dragEnterCount--;
|
||||||
// Firing here would break due to child-boundary
|
|
||||||
// and virtualizer re-render events.
|
// Each child-boundary crossing fires a
|
||||||
|
// paired dragenter/dragleave. The counter
|
||||||
|
// only reaches 0 when the cursor truly
|
||||||
|
// leaves the panel.
|
||||||
|
if (this.dragEnterCount <= 0) {
|
||||||
|
this.dragEnterCount = 0;
|
||||||
|
this.cleanupDragState();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPanelDrop = (e: DragEvent) => {
|
private onPanelDrop = (e: DragEvent) => {
|
||||||
@@ -995,6 +1010,7 @@ export class QueuePanel
|
|||||||
if (!this.dragOver) return;
|
if (!this.dragOver) return;
|
||||||
|
|
||||||
this.dragOver = false;
|
this.dragOver = false;
|
||||||
|
this.dragEnterCount = 0;
|
||||||
this.dropTargetIndex = -1;
|
this.dropTargetIndex = -1;
|
||||||
|
|
||||||
if (this.dropTargetRafId) {
|
if (this.dropTargetRafId) {
|
||||||
@@ -1255,21 +1271,14 @@ export class QueuePanel
|
|||||||
: nothing}
|
: nothing}
|
||||||
</wa-popup>
|
</wa-popup>
|
||||||
|
|
||||||
<div class="drop-indicator">
|
|
||||||
Drop tracks here to add to queue
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${tracks.length === 0
|
${tracks.length === 0
|
||||||
? html`
|
? html`<div class="empty-state">
|
||||||
<div class="empty-state">
|
<div class="drop-zone-icon">
|
||||||
<wa-icon name="list"></wa-icon>
|
<wa-icon
|
||||||
<p>Queue is empty</p>
|
name="plus"
|
||||||
<p style="font-size: 12px;">
|
></wa-icon>
|
||||||
Click a track to start
|
|
||||||
playing
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
</div>`
|
||||||
: html`
|
: html`
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
scroller
|
scroller
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ interface NavItem {
|
|||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_WIDTH = 120;
|
const MIN_WIDTH = 56;
|
||||||
const MAX_WIDTH = 400;
|
const MAX_WIDTH = 400;
|
||||||
const DEFAULT_WIDTH = 200;
|
const DEFAULT_WIDTH = 200;
|
||||||
|
const COLLAPSE_WIDTH = 142;
|
||||||
|
|
||||||
@customElement('app-sidebar')
|
@customElement('app-sidebar')
|
||||||
export class AppSidebar extends LitElement {
|
export class AppSidebar extends LitElement {
|
||||||
@@ -84,10 +85,31 @@ export class AppSidebar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li.drag-hover {
|
li.drag-hover {
|
||||||
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
background-color: var(
|
||||||
|
--yj-accent-bg-strong,
|
||||||
|
rgba(255, 212, 59, 0.15)
|
||||||
|
);
|
||||||
outline: 1px dashed var(--yj-accent, #ffd43b);
|
outline: 1px dashed var(--yj-accent, #ffd43b);
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Icon-only collapsed mode */
|
||||||
|
:host(.collapsed) ul {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.collapsed) li {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.collapsed) li p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(.collapsed) li wa-icon {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/** Delay in ms before a drag-hover triggers navigation. */
|
/** Delay in ms before a drag-hover triggers navigation. */
|
||||||
@@ -99,6 +121,9 @@ export class AppSidebar extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
private isDragging = false;
|
private isDragging = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private collapsed = false;
|
||||||
|
|
||||||
/** Whether a track drag is in progress somewhere in the app. */
|
/** Whether a track drag is in progress somewhere in the app. */
|
||||||
@state()
|
@state()
|
||||||
private trackDragActive = false;
|
private trackDragActive = false;
|
||||||
@@ -155,6 +180,10 @@ export class AppSidebar extends LitElement {
|
|||||||
this.clearDragHoverTimer();
|
this.clearDragHoverTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override updated() {
|
||||||
|
this.classList.toggle('collapsed', this.collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@@ -163,33 +192,33 @@ export class AppSidebar extends LitElement {
|
|||||||
></div>
|
></div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.navItems.map((item) => {
|
${this.navItems.map((item) => {
|
||||||
const classes = [
|
const classes = [
|
||||||
this.activeView === item.id
|
this.activeView === item.id
|
||||||
? 'active'
|
? 'active'
|
||||||
: '',
|
: '',
|
||||||
this.dragHoverView === item.id
|
this.dragHoverView === item.id
|
||||||
? 'drag-hover'
|
? 'drag-hover'
|
||||||
: '',
|
: '',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<li
|
<li
|
||||||
class=${classes}
|
class=${classes}
|
||||||
@click=${() =>
|
@click=${() =>
|
||||||
this.navigate(item.id)}
|
this.navigate(item.id)}
|
||||||
@dragover=${(e: DragEvent) =>
|
@dragover=${(e: DragEvent) =>
|
||||||
this.onNavDragOver(
|
this.onNavDragOver(
|
||||||
e,
|
e,
|
||||||
item.id,
|
item.id,
|
||||||
)}
|
)}
|
||||||
@dragleave=${() =>
|
@dragleave=${() =>
|
||||||
this.onNavDragLeave(
|
this.onNavDragLeave(
|
||||||
item.id,
|
item.id,
|
||||||
)}
|
)}
|
||||||
@drop=${(e: DragEvent) =>
|
@drop=${(e: DragEvent) =>
|
||||||
this.onNavDrop(e)}
|
this.onNavDrop(e)}
|
||||||
>
|
>
|
||||||
<wa-icon
|
<wa-icon
|
||||||
name=${item.icon}
|
name=${item.icon}
|
||||||
@@ -197,7 +226,7 @@ export class AppSidebar extends LitElement {
|
|||||||
<p>${item.label}</p>
|
<p>${item.label}</p>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -212,9 +241,13 @@ export class AppSidebar extends LitElement {
|
|||||||
|
|
||||||
const rect = this.getBoundingClientRect();
|
const rect = this.getBoundingClientRect();
|
||||||
const newWidth = e.clientX - rect.left;
|
const newWidth = e.clientX - rect.left;
|
||||||
const clampedWidth = Math.min(Math.max(newWidth, MIN_WIDTH), MAX_WIDTH);
|
const clampedWidth = Math.min(
|
||||||
|
Math.max(newWidth, MIN_WIDTH),
|
||||||
|
MAX_WIDTH,
|
||||||
|
);
|
||||||
|
|
||||||
this.style.width = `${clampedWidth}px`;
|
this.style.width = `${clampedWidth}px`;
|
||||||
|
this.collapsed = clampedWidth < COLLAPSE_WIDTH;
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleMouseUp = () => {
|
private handleMouseUp = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user