Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,29 @@ function trackUsageEvent(eventName, params) {
}
}

// Row download buttons fetch (and sometimes zip) before the browser saves a file.
// Without a busy state the button looks unresponsive for the whole wait, so users
// click again and start a second download of the same preset.
function setRowDownloadBusy(link, busy) {
if (!link) return;
if (busy) {
link.classList.add('is-busy');
link.setAttribute('aria-busy', 'true');
} else {
link.classList.remove('is-busy');
link.removeAttribute('aria-busy');
}
}

function isRowDownloadBusy(link) {
return !!link && link.classList.contains('is-busy');
}

function initBannerCta() {
var cta = document.getElementById('layerhub-banner-cta');
if (!cta) return;
cta.addEventListener('click', function () {
trackGaEvent('banner_explore', { destination: 'layerhub3d.com' });
trackUsageEvent('banner_explore', { destination: 'layerhub3d.com' });
});
}

Expand Down Expand Up @@ -1830,12 +1848,14 @@ function init() {
var directJsonLink = e.target.closest('a[data-download-url]');
if (directJsonLink) {
e.preventDefault();
if (isRowDownloadBusy(directJsonLink)) return;
var djUrl = directJsonLink.getAttribute('data-download-url');
var djFilename = directJsonLink.getAttribute('data-download-filename') || 'preset.json';
if (!djUrl || djUrl === '#') {
alert(t('alert.invalid.url'));
return;
}
setRowDownloadBusy(directJsonLink, true);
fetch(djUrl, { mode: 'cors' })
.then(function (r) {
if (!r.ok) throw new Error('Failed to fetch preset: ' + r.statusText);
Expand All @@ -1858,9 +1878,11 @@ function init() {
model: rowContext.model
});
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
setRowDownloadBusy(directJsonLink, false);
})
.catch(function (err) {
console.error('Error downloading JSON:', err);
setRowDownloadBusy(directJsonLink, false);
alert(t('alert.error.preset', { msg: err.message }));
});
return;
Expand All @@ -1871,6 +1893,7 @@ function init() {
var bambuJsonLink = e.target.closest('a[data-bambu-json="1"]');
if (bambuJsonLink) {
e.preventDefault();
if (isRowDownloadBusy(bambuJsonLink)) return;
var bjUrl = bambuJsonLink.getAttribute('data-bundle-url');
var bjFilename = bambuJsonLink.getAttribute('data-bundle-filename') || 'preset.json';
var bjMaterial = bambuJsonLink.getAttribute('data-bundle-material') || '';
Expand All @@ -1881,6 +1904,7 @@ function init() {
}

function doDownloadBambuJson() {
setRowDownloadBusy(bambuJsonLink, true);
fetch(bjUrl, { mode: 'cors' })
.then(function (r) {
if (!r.ok) throw new Error('Failed to fetch preset: ' + r.statusText);
Expand Down Expand Up @@ -1914,10 +1938,12 @@ function init() {
model: rowContext.model
});
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
setRowDownloadBusy(bambuJsonLink, false);
});
})
.catch(function (err) {
console.error('Error downloading JSON:', err);
setRowDownloadBusy(bambuJsonLink, false);
alert(t('alert.error.preset', { msg: err.message }));
});
}
Expand All @@ -1932,6 +1958,7 @@ function init() {
var bundleLink = e.target.closest('a.btn-bundle');
if (bundleLink) {
e.preventDefault();
if (isRowDownloadBusy(bundleLink)) return;
var url = bundleLink.getAttribute('data-bundle-url');
var filename = bundleLink.getAttribute('data-bundle-filename');
var material = bundleLink.getAttribute('data-bundle-material');
Expand All @@ -1943,6 +1970,9 @@ function init() {
}

function doDownloadBundle() {
// Busy until downloadAsBbsflmt takes over — from there its own modals
// and progress are the visible feedback.
setRowDownloadBusy(bundleLink, true);
// Fetch the preset JSON to get filament_vendor
fetch(url, { mode: 'cors' })
.then(function (r) {
Expand All @@ -1962,10 +1992,12 @@ function init() {
filament_vendor: data.filament_vendor || ['Polymaker'],
presetData: data
};
setRowDownloadBusy(bundleLink, false);
downloadAsBbsflmt([preset]);
})
.catch(function (err) {
console.error('Error downloading bundle:', err);
setRowDownloadBusy(bundleLink, false);
alert(t('alert.error.preset', { msg: err.message }));
});
}
Expand Down
32 changes: 32 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,38 @@ body.theme-wiki .preset-table th {
box-shadow: 0 2px 8px rgba(0, 204, 204, 0.4);
}

/* Busy state while a row download fetches and packs its file, so the button
visibly reacts to the first click instead of looking unresponsive. */
.preset-table td.td-actions .btn-download.is-busy,
.preset-table td.td-actions .btn-bundle.is-busy {
opacity: 0.6;
cursor: progress;
pointer-events: none;
}

.preset-table td.td-actions .btn-download.is-busy::after,
.preset-table td.td-actions .btn-bundle.is-busy::after {
content: '';
width: 0.7em;
height: 0.7em;
margin-left: 0.4em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: row-download-spin 0.7s linear infinite;
}

@keyframes row-download-spin {
to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
.preset-table td.td-actions .btn-download.is-busy::after,
.preset-table td.td-actions .btn-bundle.is-busy::after {
animation: none;
}
}

body.theme-wiki .preset-table td.td-actions .btn-download {
color: #006E6E;
background: rgba(0, 120, 124, 0.1);
Expand Down
6 changes: 4 additions & 2 deletions tests/banner-subscribe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ describe('app.js CTA wiring', () => {
it('initializes the banner CTA and tracks banner_explore', () => {
assert.ok(appContent.includes('function initBannerCta()'));
assert.ok(appContent.includes('initBannerCta();'));
assert.ok(appContent.includes("trackGaEvent('banner_explore'"));
assert.ok(appContent.includes("trackUsageEvent('banner_explore'"));
assert.ok(appContent.includes("getElementById('layerhub-banner-cta')"));
// trackGaEvent was never defined, so the CTA threw instead of tracking.
assert.ok(!appContent.includes('trackGaEvent'));
assert.ok(!appContent.includes('function initBannerSubscribe()'));
assert.ok(!appContent.includes("trackGaEvent('banner_subscribe'"));
assert.ok(!appContent.includes("trackUsageEvent('banner_subscribe'"));
assert.ok(!appContent.includes('isSubscribeHoneypotFilled'));
});
});
40 changes: 40 additions & 0 deletions tests/row-download-busy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, '..');
const appContent = fs.readFileSync(path.join(root, 'app.js'), 'utf-8');
const styleContent = fs.readFileSync(path.join(root, 'style.css'), 'utf-8');

describe('Row download busy state', () => {
it('exposes helpers that mark a row download button busy', () => {
assert.ok(appContent.includes('function setRowDownloadBusy(link, busy)'));
assert.ok(appContent.includes('function isRowDownloadBusy(link)'));
assert.ok(appContent.includes("link.classList.add('is-busy')"));
assert.ok(appContent.includes("link.setAttribute('aria-busy', 'true')"));
});

it('guards every async row download against a repeat click', () => {
assert.ok(appContent.includes('if (isRowDownloadBusy(directJsonLink)) return;'));
assert.ok(appContent.includes('if (isRowDownloadBusy(bambuJsonLink)) return;'));
assert.ok(appContent.includes('if (isRowDownloadBusy(bundleLink)) return;'));
});

it('clears the busy state on both success and failure', () => {
['directJsonLink', 'bambuJsonLink', 'bundleLink'].forEach((link) => {
assert.ok(appContent.includes('setRowDownloadBusy(' + link + ', true)'));
const cleared = appContent.split('setRowDownloadBusy(' + link + ', false)').length - 1;
assert.ok(cleared >= 2, link + ' must clear busy on success and on error');
});
});

it('styles the busy state in a theme-neutral way', () => {
assert.ok(styleContent.includes('.btn-download.is-busy'));
assert.ok(styleContent.includes('.btn-bundle.is-busy'));
assert.ok(styleContent.includes('@keyframes row-download-spin'));
assert.ok(styleContent.includes('prefers-reduced-motion'));
});
});
Loading