feat(tonedownload): Tone3000 NAM + IR browser, real anon key, tests
- Tonedownload.py: Tone3000 API client with search/download/cache/enrich - Server.py: 4 endpoints (search models, search IRs, install model, install IR) - Models.html + irs.html: Browse Tone3000 tab with search, trending, offline - Models.js + irs.js: Tone3000 search UI with install tracking via localStorage - Style.css: Card layout for search results with thumbnail, meta, install btn - Tests: 47 tests covering data classes, cache, API, search, download, enrichment - Anon key: Updated from placeholder to real Supabase anon key (verified: API works)
This commit is contained in:
@@ -31,6 +31,13 @@ from ..dsp.ir_loader import IRLoader
|
||||
from ..dsp.pipeline import AudioPipeline
|
||||
from ..presets.manager import PresetManager
|
||||
from ..presets.types import FXBlock, FXType, Preset
|
||||
from ..system.tonedownload import (
|
||||
Tone3000Client,
|
||||
ModelResult,
|
||||
IRResult,
|
||||
discover_anon_key,
|
||||
format_size,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -118,6 +125,7 @@ class WebServer:
|
||||
self._manager = ConnectionManager()
|
||||
self._server: Optional[uvicorn.Server] = None
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._tonedownload: Optional[Tone3000Client] = None
|
||||
self._app = self._build_app()
|
||||
|
||||
# ── App factory ─────────────────────────────────────────────────────
|
||||
@@ -750,6 +758,129 @@ class WebServer:
|
||||
self._manager.disconnect(ws)
|
||||
logger.info("WebSocket client disconnected (%d active)", self._manager.active_count)
|
||||
|
||||
# ── Tone3000 downloader endpoints ───────────────────────
|
||||
|
||||
@app.get("/api/tonedownload/status")
|
||||
async def tonedownload_status():
|
||||
"""Check if the Tone3000 API is reachable."""
|
||||
client = await self._get_tonedownload()
|
||||
if not client:
|
||||
return {"online": False, "error": "Client not initialized"}
|
||||
ok = await client.ping()
|
||||
return {"online": ok}
|
||||
|
||||
@app.get("/api/models/tonedownload/search")
|
||||
async def tonedownload_search_models(q: str = "", page: int = 0):
|
||||
"""Search Tone3000 for NAM models."""
|
||||
client = await self._get_tonedownload()
|
||||
if not client:
|
||||
raise HTTPException(status_code=503, detail="Tone3000 client unavailable")
|
||||
if not q.strip():
|
||||
results = await client.get_trending_models()
|
||||
else:
|
||||
results = await client.search_models(q.strip(), page=page)
|
||||
return {
|
||||
"results": [
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.display_name,
|
||||
"filename": r.filename,
|
||||
"size": r.size_bytes,
|
||||
"size_display": format_size(r.size_bytes),
|
||||
"author": r.author_username or "Unknown",
|
||||
"author_avatar": r.author_avatar_url,
|
||||
"thumbnail": r.thumbnail_url,
|
||||
"download_url": r.download_url,
|
||||
"tone_description": r.tone_description,
|
||||
"pack_name": r.pack_name,
|
||||
"architecture": r.architecture,
|
||||
"created_at": r.created_at,
|
||||
}
|
||||
for r in results
|
||||
],
|
||||
}
|
||||
|
||||
@app.get("/api/irs/tonedownload/search")
|
||||
async def tonedownload_search_irs(q: str = "", page: int = 0):
|
||||
"""Search Tone3000 for IRs."""
|
||||
client = await self._get_tonedownload()
|
||||
if not client:
|
||||
raise HTTPException(status_code=503, detail="Tone3000 client unavailable")
|
||||
if not q.strip():
|
||||
results = await client.get_trending_irs()
|
||||
else:
|
||||
results = await client.search_irs(q.strip(), page=page)
|
||||
return {
|
||||
"results": [
|
||||
{
|
||||
"id": r.id,
|
||||
"name": r.display_name,
|
||||
"filename": r.filename,
|
||||
"size": r.size_bytes,
|
||||
"size_display": format_size(r.size_bytes),
|
||||
"author": r.author_username or "Unknown",
|
||||
"author_avatar": r.author_avatar_url,
|
||||
"thumbnail": r.thumbnail_url,
|
||||
"download_url": r.download_url,
|
||||
"tone_description": r.tone_description,
|
||||
"created_at": r.created_at,
|
||||
}
|
||||
for r in results
|
||||
],
|
||||
}
|
||||
|
||||
@app.post("/api/models/tonedownload/install")
|
||||
async def tonedownload_install_model(data: dict):
|
||||
"""Download a .nam file to the pedal's models directory."""
|
||||
client = await self._get_tonedownload()
|
||||
if not client:
|
||||
raise HTTPException(status_code=503, detail="Tone3000 client unavailable")
|
||||
url = data.get("download_url", "")
|
||||
name = data.get("name", "model.nam")
|
||||
if not url:
|
||||
raise HTTPException(status_code=400, detail="download_url required")
|
||||
model = ModelResult(
|
||||
id=data.get("id", 0),
|
||||
name=name,
|
||||
download_url=url,
|
||||
size_bytes=data.get("size"),
|
||||
pack_name=data.get("pack_name"),
|
||||
tone_title=data.get("display_name", name),
|
||||
)
|
||||
path = await client.download_model(model)
|
||||
if not path:
|
||||
raise HTTPException(status_code=500, detail="Download failed")
|
||||
# Refresh the model list via NAM host
|
||||
nam = self.deps.nam_host
|
||||
if nam:
|
||||
nam.list_available_models()
|
||||
return {"ok": True, "path": str(path), "filename": path.name}
|
||||
|
||||
@app.post("/api/irs/tonedownload/install")
|
||||
async def tonedownload_install_ir(data: dict):
|
||||
"""Download a .wav IR to the pedal's IRs directory."""
|
||||
client = await self._get_tonedownload()
|
||||
if not client:
|
||||
raise HTTPException(status_code=503, detail="Tone3000 client unavailable")
|
||||
url = data.get("download_url", "")
|
||||
name = data.get("name", "ir.wav")
|
||||
if not url:
|
||||
raise HTTPException(status_code=400, detail="download_url required")
|
||||
ir = IRResult(
|
||||
id=0,
|
||||
name=name,
|
||||
download_url=url,
|
||||
size_bytes=data.get("size"),
|
||||
tone_title=data.get("display_name", name),
|
||||
)
|
||||
path = await client.download_ir(ir)
|
||||
if not path:
|
||||
raise HTTPException(status_code=500, detail="Download failed")
|
||||
ir_loader = self.deps.ir_loader
|
||||
if ir_loader:
|
||||
ir_loader.get_irs()
|
||||
return {"ok": True, "path": str(path), "filename": path.name}
|
||||
|
||||
return app
|
||||
|
||||
# ── Network/BT state gathering ──────────────────────────────────
|
||||
@@ -847,6 +978,21 @@ class WebServer:
|
||||
"bluetooth": self._gather_bt_state(),
|
||||
}
|
||||
|
||||
# ── Tone3000 client lazy init ────────────────────────────────
|
||||
|
||||
async def _get_tonedownload(self) -> Optional[Tone3000Client]:
|
||||
"""Lazily initialize the Tone3000 download client."""
|
||||
if self._tonedownload is None:
|
||||
from aiohttp import ClientSession
|
||||
async with ClientSession() as session:
|
||||
key = await discover_anon_key(session)
|
||||
if not key:
|
||||
# Fall back to hardcoded key
|
||||
from ..system.tonedownload import SUPABASE_ANON_KEY
|
||||
key = SUPABASE_ANON_KEY
|
||||
self._tonedownload = Tone3000Client(anon_key=key)
|
||||
return self._tonedownload
|
||||
|
||||
# ── Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
def start(self) -> None:
|
||||
|
||||
+111
-1
@@ -92,4 +92,114 @@ async function uploadIR() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadIRs);
|
||||
document.addEventListener('DOMContentLoaded', loadIRs);
|
||||
|
||||
// ── Tone3000 Browser ────────────────────────────────────────────────
|
||||
|
||||
const INSTALLED_IRS_KEY = 'tonedownload_installed_irs';
|
||||
|
||||
function getInstalledIRs() {
|
||||
try { return JSON.parse(localStorage.getItem(INSTALLED_IRS_KEY) || '[]'); } catch { return []; }
|
||||
}
|
||||
|
||||
function markIRInstalled(url) {
|
||||
const installed = getInstalledIRs();
|
||||
if (!installed.includes(url)) {
|
||||
installed.push(url);
|
||||
localStorage.setItem(INSTALLED_IRS_KEY, JSON.stringify(installed));
|
||||
}
|
||||
}
|
||||
|
||||
function isIRInstalled(url) {
|
||||
return getInstalledIRs().includes(url);
|
||||
}
|
||||
|
||||
async function searchToneIRs() {
|
||||
const input = document.getElementById('ir-tone-search');
|
||||
const query = input.value.trim();
|
||||
const resultsEl = document.getElementById('ir-tone-results');
|
||||
const loaderEl = document.getElementById('ir-tone-loader');
|
||||
const offlineEl = document.getElementById('ir-tone-offline');
|
||||
|
||||
resultsEl.innerHTML = '';
|
||||
offlineEl.style.display = 'none';
|
||||
|
||||
if (!query) {
|
||||
loadTrendingIRs();
|
||||
return;
|
||||
}
|
||||
|
||||
loaderEl.style.display = 'block';
|
||||
|
||||
try {
|
||||
const data = await apiGet('/irs/tonedownload/search?q=' + encodeURIComponent(query));
|
||||
loaderEl.style.display = 'none';
|
||||
renderToneIRResults(resultsEl, data.results || []);
|
||||
} catch (e) {
|
||||
loaderEl.style.display = 'none';
|
||||
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
|
||||
offlineEl.style.display = 'block';
|
||||
} else {
|
||||
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrendingIRs() {
|
||||
const resultsEl = document.getElementById('ir-tone-results');
|
||||
const loaderEl = document.getElementById('ir-tone-loader');
|
||||
const offlineEl = document.getElementById('ir-tone-offline');
|
||||
|
||||
resultsEl.innerHTML = '';
|
||||
offlineEl.style.display = 'none';
|
||||
loaderEl.style.display = 'block';
|
||||
|
||||
try {
|
||||
const data = await apiGet('/irs/tonedownload/search?q=');
|
||||
loaderEl.style.display = 'none';
|
||||
renderToneIRResults(resultsEl, data.results || []);
|
||||
} catch (e) {
|
||||
loaderEl.style.display = 'none';
|
||||
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
|
||||
offlineEl.style.display = 'block';
|
||||
} else {
|
||||
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderToneIRResults(container, results) {
|
||||
if (!results || results.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted">No results found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const r of results) {
|
||||
const installed = isIRInstalled(r.download_url);
|
||||
const thumb = r.thumbnail
|
||||
? `<img src="${escapeHtml(r.thumbnail)}" alt="" class="tonedownload-thumb" loading="lazy" onerror="this.style.display='none'">`
|
||||
: '<div class="tonedownload-thumb tonedownload-thumb-placeholder">📊</div>';
|
||||
|
||||
html += `<div class="tonedownload-card ${installed ? 'installed' : ''}">
|
||||
${thumb}
|
||||
<div class="tonedownload-card-body">
|
||||
<div class="tonedownload-card-name">${escapeHtml(r.name)}</div>
|
||||
<div class="tonedownload-card-meta">
|
||||
${escapeHtml(r.author)} · ${escapeHtml(r.size_display)}
|
||||
</div>
|
||||
${r.tone_description ? `<div class="tonedownload-card-desc">${escapeHtml(r.tone_description.substring(0, 120))}</div>` : ''}
|
||||
</div>
|
||||
<div class="tonedownload-card-actions">
|
||||
${installed
|
||||
? '<span class="tonedownload-installed-badge">✓ Installed</span>'
|
||||
: `<button class="btn btn-primary tonedownload-install-btn"
|
||||
onclick="installToneIR('${escapeHtml(r.download_url)}', '${escapeHtml(r.name)}', ${r.size || 'null'}, '${escapeHtml(r.filename)}')">
|
||||
Install
|
||||
</button>`
|
||||
}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
+163
-1
@@ -92,4 +92,166 @@ async function uploadModel() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadModels);
|
||||
document.addEventListener('DOMContentLoaded', loadModels);
|
||||
|
||||
// ── Tone3000 Browser ────────────────────────────────────────────────
|
||||
|
||||
const INSTALLED_MODELS_KEY = 'tonedownload_installed_models';
|
||||
|
||||
function getInstalledModels() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(INSTALLED_MODELS_KEY) || '[]');
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function markInstalled(url) {
|
||||
const installed = getInstalledModels();
|
||||
if (!installed.includes(url)) {
|
||||
installed.push(url);
|
||||
localStorage.setItem(INSTALLED_MODELS_KEY, JSON.stringify(installed));
|
||||
}
|
||||
}
|
||||
|
||||
function isInstalled(url) {
|
||||
return getInstalledModels().includes(url);
|
||||
}
|
||||
|
||||
async function searchToneModels() {
|
||||
const input = document.getElementById('model-tone-search');
|
||||
const query = input.value.trim();
|
||||
const resultsEl = document.getElementById('model-tone-results');
|
||||
const loaderEl = document.getElementById('model-tone-loader');
|
||||
const offlineEl = document.getElementById('model-tone-offline');
|
||||
|
||||
resultsEl.innerHTML = '';
|
||||
offlineEl.style.display = 'none';
|
||||
|
||||
if (!query) {
|
||||
loadTrendingModels();
|
||||
return;
|
||||
}
|
||||
|
||||
loaderEl.style.display = 'block';
|
||||
|
||||
try {
|
||||
const data = await apiGet('/models/tonedownload/search?q=' + encodeURIComponent(query));
|
||||
loaderEl.style.display = 'none';
|
||||
renderToneResults(resultsEl, data.results || [], 'model');
|
||||
} catch (e) {
|
||||
loaderEl.style.display = 'none';
|
||||
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
|
||||
offlineEl.style.display = 'block';
|
||||
} else {
|
||||
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrendingModels() {
|
||||
const resultsEl = document.getElementById('model-tone-results');
|
||||
const loaderEl = document.getElementById('model-tone-loader');
|
||||
const offlineEl = document.getElementById('model-tone-offline');
|
||||
|
||||
resultsEl.innerHTML = '';
|
||||
offlineEl.style.display = 'none';
|
||||
loaderEl.style.display = 'block';
|
||||
|
||||
try {
|
||||
const data = await apiGet('/models/tonedownload/search?q=');
|
||||
loaderEl.style.display = 'none';
|
||||
renderToneResults(resultsEl, data.results || [], 'model');
|
||||
} catch (e) {
|
||||
loaderEl.style.display = 'none';
|
||||
if (e.message.includes('503') || e.message.includes('Failed to fetch')) {
|
||||
offlineEl.style.display = 'block';
|
||||
} else {
|
||||
resultsEl.innerHTML = `<p class="text-muted">Error: ${escapeHtml(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderToneResults(container, results, type) {
|
||||
if (!results || results.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted">No results found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const r of results) {
|
||||
const installed = isInstalled(r.download_url);
|
||||
const thumb = r.thumbnail
|
||||
? `<img src="${escapeHtml(r.thumbnail)}" alt="" class="tonedownload-thumb" loading="lazy" onerror="this.style.display='none'">`
|
||||
: '<div class="tonedownload-thumb tonedownload-thumb-placeholder">🎸</div>';
|
||||
|
||||
html += `<div class="tonedownload-card ${installed ? 'installed' : ''}">
|
||||
${thumb}
|
||||
<div class="tonedownload-card-body">
|
||||
<div class="tonedownload-card-name">${escapeHtml(r.name)}</div>
|
||||
<div class="tonedownload-card-meta">
|
||||
${escapeHtml(r.author)} · ${escapeHtml(r.size_display)}
|
||||
${r.architecture ? ` · ${escapeHtml(r.architecture)}` : ''}
|
||||
</div>
|
||||
${r.tone_description ? `<div class="tonedownload-card-desc">${escapeHtml(r.tone_description.substring(0, 120))}</div>` : ''}
|
||||
</div>
|
||||
<div class="tonedownload-card-actions">
|
||||
${installed
|
||||
? '<span class="tonedownload-installed-badge">✓ Installed</span>'
|
||||
: `<button class="btn btn-primary tonedownload-install-btn"
|
||||
onclick="installToneModel('${escapeHtml(r.download_url)}', '${escapeHtml(r.name)}', ${r.size || 'null'}, '${escapeHtml(r.filename)}')">
|
||||
Install
|
||||
</button>`
|
||||
}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
async function installToneModel(downloadUrl, displayName, size, filename) {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Downloading...';
|
||||
|
||||
try {
|
||||
const data = await apiPost('/models/tonedownload/install', {
|
||||
download_url: downloadUrl,
|
||||
name: filename || displayName + '.nam',
|
||||
display_name: displayName,
|
||||
size: size,
|
||||
});
|
||||
markInstalled(downloadUrl);
|
||||
btn.textContent = '✓ Done';
|
||||
btn.style.background = 'var(--success)';
|
||||
btn.style.borderColor = 'var(--success)';
|
||||
// Refresh model list
|
||||
loadModels();
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Install';
|
||||
alert('Install failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function installToneIR(downloadUrl, displayName, size, filename) {
|
||||
const btn = event.target;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Downloading...';
|
||||
|
||||
try {
|
||||
const data = await apiPost('/irs/tonedownload/install', {
|
||||
download_url: downloadUrl,
|
||||
name: filename || displayName + '.wav',
|
||||
display_name: displayName,
|
||||
size: size,
|
||||
});
|
||||
markInstalled(downloadUrl);
|
||||
btn.textContent = '✓ Done';
|
||||
btn.style.background = 'var(--success)';
|
||||
btn.style.borderColor = 'var(--success)';
|
||||
loadIRs();
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Install';
|
||||
alert('Install failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
@@ -994,4 +994,131 @@ body {
|
||||
|
||||
.bt-midi-device code {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
/* ── Tone3000 Downloader ──────────────────────────────────────────── */
|
||||
|
||||
.badge-tone3000 {
|
||||
background: linear-gradient(135deg, #0000FF, #FF0000);
|
||||
color: white;
|
||||
font-size: 0.65rem;
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.tonedownload-search-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tonedownload-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.tonedownload-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tonedownload-offline {
|
||||
padding: 10px;
|
||||
background: rgba(255, 167, 38, 0.1);
|
||||
border: 1px solid var(--warning);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--warning);
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tonedownload-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tonedownload-card {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tonedownload-card.installed {
|
||||
border-color: var(--success);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tonedownload-thumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.tonedownload-thumb-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.tonedownload-card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tonedownload-card-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tonedownload-card-meta {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.tonedownload-card-desc {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tonedownload-card-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tonedownload-install-btn {
|
||||
padding: 4px 12px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.tonedownload-installed-badge {
|
||||
display: inline-block;
|
||||
color: var(--success);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -42,6 +42,26 @@
|
||||
<button class="btn btn-primary" onclick="uploadIR()">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Browse Tone3000 <span class="badge badge-tone3000">BETA</span></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="hint">Search and download impulse responses directly from the Tone3000 community.</p>
|
||||
<div class="tonedownload-search-row">
|
||||
<input type="text" id="ir-tone-search" class="file-input tonedownload-input"
|
||||
placeholder="Search IRs (e.g. Celestion V30)..." onkeydown="if(event.key==='Enter')searchToneIRs()">
|
||||
<button class="btn btn-primary" onclick="searchToneIRs()">Search</button>
|
||||
<button class="btn btn-secondary" onclick="loadTrendingIRs()">Trending</button>
|
||||
</div>
|
||||
<div id="ir-tone-loader" class="loading" style="display:none">Searching Tone3000...</div>
|
||||
<div id="ir-tone-offline" class="tonedownload-offline" style="display:none">
|
||||
<span>⚠ Offline — search unavailable</span>
|
||||
</div>
|
||||
<div id="ir-tone-results" class="tonedownload-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
|
||||
@@ -42,6 +42,26 @@
|
||||
<button class="btn btn-primary" onclick="uploadModel()">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Browse Tone3000 <span class="badge badge-tone3000">BETA</span></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="hint">Search and download NAM models directly from the Tone3000 community.</p>
|
||||
<div class="tonedownload-search-row">
|
||||
<input type="text" id="model-tone-search" class="file-input tonedownload-input"
|
||||
placeholder="Search models (e.g. Marshall JCM800)..." onkeydown="if(event.key==='Enter')searchToneModels()">
|
||||
<button class="btn btn-primary" onclick="searchToneModels()">Search</button>
|
||||
<button class="btn btn-secondary" onclick="loadTrendingModels()">Trending</button>
|
||||
</div>
|
||||
<div id="model-tone-loader" class="loading" style="display:none">Searching Tone3000...</div>
|
||||
<div id="model-tone-offline" class="tonedownload-offline" style="display:none">
|
||||
<span>⚠ Offline — search unavailable</span>
|
||||
</div>
|
||||
<div id="model-tone-results" class="tonedownload-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
|
||||
Reference in New Issue
Block a user