From 788b7011d03fa177aaf4e3d3f7665aa242e41880 Mon Sep 17 00:00:00 2001 From: elmerohueso Date: Tue, 28 Apr 2026 17:44:08 -0600 Subject: [PATCH] fix hifi instance reorder and enable/disable --- database/music_database.py | 12 +++++ web_server.py | 21 +++++++++ webui/static/settings.js | 94 +++++++++++++++++++++++++++++++++++--- webui/static/style.css | 77 +++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 7 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 7b5b265d..ddfd9f65 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -11656,6 +11656,18 @@ class MusicDatabase: logger.error(f"Error removing HiFi instance: {e}") return False + def toggle_hifi_instance(self, url: str, enabled: bool) -> bool: + """Enable or disable a HiFi instance.""" + try: + conn = self._get_connection() + cursor = conn.cursor() + cursor.execute("UPDATE hifi_instances SET enabled = ? WHERE url = ?", (1 if enabled else 0, url)) + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"Error toggling HiFi instance: {e}") + return False + def reorder_hifi_instances(self, urls: List[str]) -> bool: """Update priorities based on the given URL order.""" try: diff --git a/web_server.py b/web_server.py index e91cb7ed..1f678d08 100644 --- a/web_server.py +++ b/web_server.py @@ -24750,6 +24750,27 @@ def hifi_remove_instance(url): return jsonify({'success': False, 'error': str(e)}), 500 +@app.route('/api/hifi/instances/toggle', methods=['POST']) +@admin_only +def hifi_toggle_instance(): + """Toggle enabled state of a HiFi API instance.""" + try: + data = request.get_json() or {} + url = data.get('url', '').strip().rstrip('/') + enabled = data.get('enabled', True) + if not url: + return jsonify({'success': False, 'error': 'URL is required'}), 400 + from database.music_database import get_database + db = get_database() + db.toggle_hifi_instance(url, enabled) + if soulseek_client and hasattr(soulseek_client, 'hifi') and soulseek_client.hifi: + soulseek_client.hifi.reload_instances() + return jsonify({'success': True}) + except Exception as e: + logger.error(f"Error toggling HiFi instance: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/hifi/instances/reorder', methods=['POST']) @admin_only def hifi_reorder_instances(): diff --git a/webui/static/settings.js b/webui/static/settings.js index 724293c3..4f916223 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -3289,22 +3289,102 @@ async function loadHiFiInstances() { return; } listEl.innerHTML = data.instances.map((inst, i) => { - const enabledClass = inst.enabled ? '' : 'opacity:0.4;'; + const enabledClass = inst.enabled ? '' : 'hifi-instance-disabled'; const checkHtml = inst.enabled - ? `` - : ``; - return `
- - ${escapeHtml(inst.url)} + ? `` + : ``; + return `
+ + ${escapeHtml(inst.url)} ${checkHtml} - +
`; }).join(''); + _initHiFiDragDrop(); } catch (e) { listEl.innerHTML = `
Error loading instances: ${escapeHtml(e.message)}
`; } } +function _initHiFiDragDrop() { + const listEl = document.getElementById('hifi-instances-list'); + if (!listEl) return; + let dragIdx = null; + + listEl.querySelectorAll('.hifi-instance-item').forEach((item, idx) => { + item.addEventListener('dragstart', (e) => { + dragIdx = idx; + item.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + }); + item.addEventListener('dragend', () => { + item.classList.remove('dragging'); + dragIdx = null; + listEl.querySelectorAll('.hifi-instance-item').forEach(i => i.classList.remove('drag-over')); + }); + item.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + item.classList.add('drag-over'); + }); + item.addEventListener('dragleave', () => { + item.classList.remove('drag-over'); + }); + item.addEventListener('drop', async (e) => { + e.preventDefault(); + item.classList.remove('drag-over'); + if (dragIdx === null) return; + const items = [...listEl.querySelectorAll('.hifi-instance-item')]; + const dragged = items[dragIdx]; + if (dragIdx !== idx) { + if (dragIdx < idx) { + item.after(dragged); + } else { + item.before(dragged); + } + const urls = [...listEl.querySelectorAll('.hifi-instance-item')].map(el => el.dataset.url); + await _saveHiFiInstanceOrder(urls); + } + }); + }); +} + +async function _saveHiFiInstanceOrder(urls) { + try { + await fetch('/api/hifi/instances/reorder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ urls }) + }); + } catch (e) { + console.error('Failed to save HiFi instance order:', e); + } +} + +async function toggleHiFiInstance(url) { + const listEl = document.getElementById('hifi-instances-list'); + if (!listEl) return; + const item = listEl.querySelector(`.hifi-instance-item[data-url="${url}"]`); + const toggle = item?.querySelector('.hifi-instance-toggle'); + const currentlyEnabled = toggle?.classList.contains('on'); + const newEnabled = !currentlyEnabled; + try { + const resp = await fetch('/api/hifi/instances/toggle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, enabled: newEnabled }) + }); + const data = await resp.json(); + if (data.success) { + loadHiFiInstances(); + } else { + alert(data.error || 'Failed to toggle instance'); + } + } catch (e) { + alert(`Error: ${e.message}`); + } +} + async function addHiFiInstance() { const input = document.getElementById('hifi-new-instance'); if (!input) return; diff --git a/webui/static/style.css b/webui/static/style.css index 054e9d94..a25df5ed 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -60262,3 +60262,80 @@ body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-bt #artist-detail-page .release-card.album-card .mb-card-icon:hover { opacity: 1; } + +/* ── HiFi API instances list (drag and drop) ── */ +#hifi-instances-list { + display: flex; + flex-direction: column; + gap: 4px; +} +.hifi-instance-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + font-size: 0.82em; + user-select: none; + transition: all 0.2s; + cursor: grab; +} +.hifi-instance-item:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.1); +} +.hifi-instance-item.dragging { + opacity: 0.4; + border-color: rgb(var(--accent-rgb)); +} +.hifi-instance-item.drag-over { + border-color: rgb(var(--accent-rgb)); + background: rgba(var(--accent-rgb), 0.08); +} +.hifi-instance-item.disabled { + opacity: 0.4; +} +.hifi-instance-grip { + color: rgba(255, 255, 255, 0.3); + cursor: grab; + flex-shrink: 0; + font-size: 1em; +} +.hifi-instance-url { + flex: 1; + color: rgba(255, 255, 255, 0.7); + font-family: monospace; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.hifi-instance-toggle { + flex-shrink: 0; + cursor: pointer; + font-size: 0.9em; + transition: color 0.15s; +} +.hifi-instance-toggle.on { + color: #4caf50; +} +.hifi-instance-toggle.on:hover { + color: #66bb6a; +} +.hifi-instance-toggle.off { + color: #666; +} +.hifi-instance-toggle.off:hover { + color: #999; +} +.hifi-instance-remove { + flex-shrink: 0; + color: #f44336; + cursor: pointer; + font-size: 0.9em; + transition: color 0.15s; +} +.hifi-instance-remove:hover { + color: #ef5350; +}