mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 03:04:30 +00:00
Folder Library: enable hover preview by default
Make the hover-preview feature opt-out instead of opt-in (defaults to on). Increase dwell delay from 500ms to 800ms to prevent accidental triggers while clicking/dragging. Replace 4-bar equalizer indicator with 9-bar waveform with staggered animation and fade-in entrance. Add guards to prevent preview during drag operations. Update button styling and all user-facing docs to reflect new defaults. Signed-off-by: Kyle <kyle.j.t@live.co.uk>
This commit is contained in:
@@ -153,13 +153,15 @@ Each song object (built by `_meta()`):
|
||||
"added": 1748132400.0,
|
||||
"arrangements": ["Lead", "Rhythm", "Bass"],
|
||||
"stems": ["Drums", "Bass", "Vocals"],
|
||||
"lyrics": true
|
||||
"lyrics": true,
|
||||
"audio_member": "preview.ogg"
|
||||
}
|
||||
```
|
||||
|
||||
- `filename` is the full relative path from the DLC root — pass it directly to `window.playSong()`.
|
||||
- `added` is a Unix timestamp (float, seconds) from `stat().st_mtime` — convert with `new Date(added * 1000)`. Always recomputed fresh (it changes when a file moves), even on a metadata-cache hit.
|
||||
- `arrangements` / `stems` are flat lists of **strings**, even though `extract_meta()` returns them as objects.
|
||||
- `audio_member` is the best in-pack audio file for **hover-preview**, resolved by `_audio_member()` (`preview.ogg` → `stems/full.ogg` → `stems/audio.mp3` → any stem; `null` if the pack has no audio). Fetch it as `/api/sloppak/<filename>/file/<audio_member>` (both path-encoded per segment). Resolving it server-side means the frontend previews with **one** request instead of probing (and 404ing) a hardcoded path. Cached with the rest of the meta.
|
||||
|
||||
### extract_meta returns arrangements/stems as objects, not strings
|
||||
|
||||
@@ -329,13 +331,24 @@ Drag-and-drop uses **pointer events** (mousedown/mousemove/mouseup), not the HTM
|
||||
- **Esc cancels** — resolves with `null`, same as Cancel (applies to rename, delete, create folder/subfolder, move song)
|
||||
- **Enter confirms** — submits, equivalent to OK
|
||||
|
||||
## Preview on Hover
|
||||
|
||||
Hovering a song for `_HOVER_PREVIEW_DELAY_MS` (800 ms) plays a short audio preview in place — no navigation to the player. Toggled by a play-icon toolbar button (`_injectToolbar`); **on by default** (`_previewHover`, persisted per surface under `<cfg.storePrefix>previewHover` — only an explicit stored `'false'` disables it).
|
||||
|
||||
- **Audio** — a dedicated `_previewAudio` `<audio>` element (never the main player's). Source is `song.audio_member` (backend-resolved; see Song Metadata) fetched from `/api/sloppak/<file>/file/<member>`, played **from 0**. Don't seek by `song.duration` — the member is usually a short `preview.ogg` clip, so a full-song offset lands past its end and nothing plays.
|
||||
- **Sequence guard** — `_previewSeq` is bumped on every start/stop, so a slow load that resolves after the pointer has moved on is ignored.
|
||||
- **Drag / click safety** — `mouseenter` skips arming while a drag is in progress (`_dragState` non-null); `mousedown` clears the pending dwell timer **and** `_stopPreview()`s any already-playing one, so grabbing a song to drag (or clicking to play) never leaves a preview running.
|
||||
- **Indicator** — a waveform overlay (`.fl-wf`, 9 bars) over the art, drawn via a one-time injected `<style>` (`_ensurePreviewStyle`). It **fades in** (`fl-in`) to avoid an abrupt pop, and the bars animate only once audio actually fires (the `.playing` class, set on the `playing` event). Perf: bars animate with `transform: scaleY` + `will-change: transform` (GPU-composited, no per-frame JS), only while playing. `_showIndicator` / `_markIndicatorPlaying` / `_clearIndicator` manage it against `_previewIndHost`.
|
||||
- **CSS gotcha** — set the bar animation with **longhand** `animation-name`/`-duration`/… , not the `animation` shorthand: the shorthand resets `animation-delay` to 0 and (being higher-specificity) overrode the per-bar `nth-child` delays, making every bar move in sync.
|
||||
|
||||
Changing `screen.js` needs a re-fetch: the host loads it as `screen.js?v=<plugin.json version>`, so **bump `plugin.json` `version`** to ship a change to users (or hard-refresh while developing).
|
||||
|
||||
## Roadmap
|
||||
|
||||
Implemented since the original release: **nested subfolders** (recursive tree + create-inside-folder), drag-and-drop, sort, advanced filtering, server-side tree filtering synced to the host library, and the warm metadata cache.
|
||||
Implemented since the original release: **nested subfolders** (recursive tree + create-inside-folder), drag-and-drop, sort, advanced filtering, server-side tree filtering synced to the host library, the warm metadata cache, and **preview-on-hover** (see above).
|
||||
|
||||
Not yet implemented, in rough priority order:
|
||||
|
||||
- **Auto-play on hover** — with an on/off toggle saved to localStorage.
|
||||
- **Bulk move** — multi-select songs and move them all at once.
|
||||
- **Thumbnail performance** — faster loading and smoother scrolling with large libraries.
|
||||
- **Adjustable thumbnail/row sizes** — user-resizable song cards and list rows.
|
||||
|
||||
@@ -30,7 +30,7 @@ A FeedBack (fee[dB]ack) plugin that organizes your `.sloppak` / `.feedpak` DLC s
|
||||
- **List & Grid views** — toggle between a compact list with thumbnails or a full album art card grid
|
||||
- **Album art** — pulls art automatically for every song in both views
|
||||
- **One-click playback** — click any song to start playing immediately
|
||||
- **Preview on hover** — optional: hover a song briefly to preview its audio in place; toggle on/off from the toolbar (off by default)
|
||||
- **Preview song on hover** — hover a song briefly to preview its audio in place, with a waveform indicator over the art; toggle from the toolbar (on by default)
|
||||
- **Sort options** — sort songs by title, artist, duration, year, tuning, or recently added with an asc/desc toggle
|
||||
- **Advanced filters** — filter by arrangements, stems, lyrics, and tuning with include and exclude support
|
||||
- **Folder management** — create, rename, and delete folders without leaving the plugin
|
||||
@@ -55,7 +55,7 @@ Folder Library ships bundled with FeedBack as a core plugin (`"bundled": true`),
|
||||
| Switch to grid view | Click the grid icon in the toolbar |
|
||||
| Switch to list view | Click the list icon in the toolbar |
|
||||
| Play a song | Click any song row or card |
|
||||
| Preview on hover | Click the ▶ toolbar button to toggle; when on, hovering a song ~0.5s previews its audio |
|
||||
| Preview song on hover | On by default; click the toolbar button to toggle. When on, hovering a song ~0.8s previews its audio |
|
||||
| Sort songs | Use the sort dropdown in the toolbar |
|
||||
| Toggle sort direction | Click the arrow button next to the sort dropdown |
|
||||
| Open filters | Click the filter icon in the toolbar |
|
||||
|
||||
@@ -50,7 +50,7 @@ function createFolderSurface(cfg) {
|
||||
let _sortDir = _store('sortDir') || 'asc';
|
||||
let _toolbarDone = false;
|
||||
let _hoveredFolder = null; // { wrap, hdr, btnGroup } — only innermost folder is active
|
||||
let _previewHover = _store('previewHover') === 'true'; // opt-in: preview a song's audio on hover
|
||||
let _previewHover = _store('previewHover') !== 'false'; // default on; preview a song's audio on hover
|
||||
let _previewTimer = null;
|
||||
let _previewAudio = null; // dedicated element — never touches the main player's <audio>
|
||||
let _previewSeq = 0; // invalidates in-flight loads when the hover moves or stops
|
||||
@@ -740,7 +740,7 @@ function createFolderSurface(cfg) {
|
||||
// pointer dwells briefly (so skimming doesn't blast audio), stopping on
|
||||
// leave. Shows an equalizer "now playing" indicator over the art. Never
|
||||
// calls playSong or touches the main player's <audio> element.
|
||||
var _HOVER_PREVIEW_DELAY_MS = 500;
|
||||
var _HOVER_PREVIEW_DELAY_MS = 800; // dwell before preview — long enough that a click/drag doesn't trigger it
|
||||
var _previewIndHost = null; // art element currently showing the indicator
|
||||
|
||||
// The backend resolves the correct in-pack audio member (song.audio_member),
|
||||
@@ -757,13 +757,14 @@ function createFolderSurface(cfg) {
|
||||
var st = document.createElement('style');
|
||||
st.id = 'fl-preview-style';
|
||||
st.textContent =
|
||||
'.fl-preview-ind{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);z-index:4;pointer-events:none;}' +
|
||||
'.fl-eq{display:flex;align-items:flex-end;gap:2px;height:45%;max-height:22px;opacity:.55;}' +
|
||||
'.fl-preview-ind.playing .fl-eq{opacity:1;}' +
|
||||
'.fl-eq i{display:block;width:3px;height:25%;background:#60a5fa;border-radius:1px;}' +
|
||||
'.fl-preview-ind.playing .fl-eq i{animation:fl-eq-b .8s ease-in-out infinite;}' +
|
||||
'.fl-eq i:nth-child(2){animation-delay:.15s;}.fl-eq i:nth-child(3){animation-delay:.3s;}.fl-eq i:nth-child(4){animation-delay:.45s;}' +
|
||||
'@keyframes fl-eq-b{0%,100%{height:25%}50%{height:100%}}';
|
||||
'.fl-preview-ind{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);z-index:4;pointer-events:none;animation:fl-in .28s ease both;}' +
|
||||
'.fl-wf{display:flex;align-items:center;gap:2px;height:55%;max-height:22px;opacity:.55;transition:opacity .2s ease;}' +
|
||||
'.fl-preview-ind.playing .fl-wf{opacity:1;}' +
|
||||
'.fl-wf i{display:block;width:2px;height:100%;background:#60a5fa;border-radius:2px;transform:scaleY(.4);transform-origin:center;}' +
|
||||
'.fl-preview-ind.playing .fl-wf i{animation-name:fl-wf;animation-duration:1s;animation-timing-function:ease-in-out;animation-iteration-count:infinite;will-change:transform;}' +
|
||||
'.fl-wf i:nth-child(odd){animation-delay:-.2s}.fl-wf i:nth-child(3n){animation-delay:-.4s}.fl-wf i:nth-child(4n){animation-delay:-.1s}' +
|
||||
'@keyframes fl-wf{0%,100%{transform:scaleY(.4)}50%{transform:scaleY(1)}}' +
|
||||
'@keyframes fl-in{from{opacity:0}to{opacity:1}}';
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
function _showIndicator(host) {
|
||||
@@ -772,7 +773,7 @@ function createFolderSurface(cfg) {
|
||||
_ensurePreviewStyle();
|
||||
var ind = document.createElement('div');
|
||||
ind.className = 'fl-preview-ind';
|
||||
ind.innerHTML = '<span class="fl-eq"><i></i><i></i><i></i><i></i></span>';
|
||||
ind.innerHTML = '<span class="fl-wf"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>';
|
||||
host.appendChild(ind);
|
||||
_previewIndHost = host;
|
||||
}
|
||||
@@ -823,16 +824,19 @@ function createFolderSurface(cfg) {
|
||||
}
|
||||
function _armHoverPreview(el, song, host) {
|
||||
el.addEventListener('mouseenter', function () {
|
||||
if (!_previewHover) return;
|
||||
if (!_previewHover || _dragState) return; // don't preview while dragging a song
|
||||
clearTimeout(_previewTimer);
|
||||
_previewTimer = setTimeout(function () {
|
||||
if (_previewHover) _startPreview(song, host);
|
||||
if (_previewHover && !_dragState) _startPreview(song, host);
|
||||
}, _HOVER_PREVIEW_DELAY_MS);
|
||||
});
|
||||
el.addEventListener('mouseleave', function () {
|
||||
clearTimeout(_previewTimer);
|
||||
_stopPreview();
|
||||
});
|
||||
// A click or drag-start cancels a pending preview AND stops one that's
|
||||
// already playing, so it never runs mid-interaction.
|
||||
el.addEventListener('mousedown', function () { clearTimeout(_previewTimer); _stopPreview(); });
|
||||
}
|
||||
|
||||
function _songCard(song, folderName) {
|
||||
@@ -1695,10 +1699,11 @@ function createFolderSurface(cfg) {
|
||||
previewBtn.style.cssText = 'display:flex; align-items:center; gap:6px; padding:7px 12px; border:1px solid #374151; border-radius:10px; cursor:pointer; font-size:13px; white-space:nowrap; transition:color 0.1s, background 0.1s, border-color 0.1s;';
|
||||
previewBtn.innerHTML = '<svg viewBox="0 0 20 20" fill="currentColor" style="width:14px;height:14px"><path d="M6 4l10 6-10 6V4z"/></svg>';
|
||||
function _applyPreviewBtn() {
|
||||
previewBtn.style.background = _previewHover ? '#1d4ed8' : '#1f2937';
|
||||
previewBtn.style.color = _previewHover ? '#ffffff' : '#9ca3af';
|
||||
previewBtn.style.borderColor = _previewHover ? '#3b82f6' : '#374151';
|
||||
previewBtn.title = 'Preview on hover: ' + (_previewHover ? 'on' : 'off');
|
||||
previewBtn.style.background = _previewHover ? '#1d4ed8' : '#171a22';
|
||||
previewBtn.style.color = _previewHover ? '#ffffff' : '#565f6d';
|
||||
previewBtn.style.borderColor = _previewHover ? '#3b82f6' : '#2a303b';
|
||||
previewBtn.style.opacity = _previewHover ? '1' : '0.5';
|
||||
previewBtn.title = 'Preview song on hover: ' + (_previewHover ? 'on' : 'off');
|
||||
}
|
||||
_applyPreviewBtn();
|
||||
previewBtn.addEventListener('click', function () {
|
||||
|
||||
Reference in New Issue
Block a user