Two small additions to the project-filter / channel-selector flow that
already worked end-to-end for HTTP-mode but were missing in the local
File-System-Access path and across landing→archive navigation:
* archive: scanLocalRecursive now applies window.app.projectFilter at
depth 0, mirroring the HTTP source's existing filter at source.js:316.
Loading archive.html?projects=A,B in local mode (file://) now virtually
merges A and B into one combined view, same as HTTP mode does today.
* landing: openArchive() reads ?v= from its own URL and passes it through
to the archive.html link it generates. This keeps the user on the same
channel (alpha/beta/stable/<version>) when they cross from the project
picker to the archive — without it, alpha-channel users would silently
drop back to whatever the deployment-default channel is at the
archive.html boundary.
Test exercises the local-mode filter via the existing mock-fs-api
fixture: three top-level projects, projectFilter set to {A, B}, scan
produces only A's and B's files. (The url-state.restore() URL parsing
path is well-trodden in the HTTP case — the test sets projectFilter
directly to isolate the new source.js change from a pre-existing init()
fragility in the mock environment.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
9.1 KiB
JavaScript
222 lines
9.1 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
var accessibleProjects = []; // [{name, url}, ...] from server
|
|
var presets = []; // [{name, projects[]}, ...] from localStorage
|
|
var PRESETS_KEY = 'zddc_landing_presets';
|
|
|
|
// ── Initialise ──────────────────────────────────────────────────────────
|
|
|
|
async function init() {
|
|
loadPresets();
|
|
|
|
var urlProjects = getUrlProjects();
|
|
|
|
var projectList = document.getElementById('projectList');
|
|
projectList.innerHTML = '<div class="project-list-loading">Loading projects…<\/div>';
|
|
|
|
try {
|
|
var resp = await fetch(location.origin + location.pathname.replace(/\/[^\/]*$/, '/'), {
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
accessibleProjects = await resp.json();
|
|
} catch (e) {
|
|
projectList.innerHTML = '<div class="project-list-empty">Could not load project list: ' + escapeHtml(e.message) + '<\/div>';
|
|
return;
|
|
}
|
|
|
|
// Warn about URL projects that are not accessible
|
|
if (urlProjects.size > 0) {
|
|
var missing = Array.from(urlProjects).filter(function(p) {
|
|
return !accessibleProjects.some(function(ap) { return ap.name === p; });
|
|
});
|
|
if (missing.length > 0) {
|
|
showWarning('This link includes projects you don\'t have access to: ' + missing.map(escapeHtml).join(', '));
|
|
}
|
|
}
|
|
|
|
renderProjects(urlProjects);
|
|
renderPresetMenu();
|
|
|
|
// Close preset menu on outside click
|
|
document.addEventListener('click', function(e) {
|
|
var menu = document.getElementById('presetMenu');
|
|
var btn = document.getElementById('presetMenuBtn');
|
|
if (menu && !menu.classList.contains('hidden') && !menu.contains(e.target) && e.target !== btn) {
|
|
menu.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
function getUrlProjects() {
|
|
var params = new URLSearchParams(location.search);
|
|
var val = params.get('projects');
|
|
if (!val) return new Set();
|
|
return new Set(val.split(',').map(function(p) { return p.trim(); }).filter(Boolean));
|
|
}
|
|
|
|
// ── Rendering ────────────────────────────────────────────────────────────
|
|
|
|
function renderProjects(preCheck) {
|
|
var container = document.getElementById('projectList');
|
|
if (accessibleProjects.length === 0) {
|
|
container.innerHTML = '<div class="project-list-empty">No projects available.<\/div>';
|
|
return;
|
|
}
|
|
var html = accessibleProjects.map(function(p) {
|
|
var checked = (preCheck.size === 0 || preCheck.has(p.name)) ? ' checked' : '';
|
|
return '<div class="project-item" onclick="LandingApp.toggleProject(this)">' +
|
|
'<input type="checkbox" value="' + escapeHtml(p.name) + '"' + checked + ' onclick="event.stopPropagation()">' +
|
|
'<span class="project-item-name">' + escapeHtml(p.name) + '<\/span>' +
|
|
'<\/div>';
|
|
}).join('');
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderPresetMenu() {
|
|
var menu = document.getElementById('presetMenu');
|
|
if (!menu) return;
|
|
if (presets.length === 0) {
|
|
menu.innerHTML = '<div class="preset-menu-empty">No presets saved.<\/div>';
|
|
return;
|
|
}
|
|
menu.innerHTML = presets.map(function(preset) {
|
|
return '<div class="preset-menu-item">' +
|
|
'<span class="preset-menu-item-name" onclick="LandingApp.applyPreset(' + JSON.stringify(preset.name) + ')">' +
|
|
escapeHtml(preset.name) + '<\/span>' +
|
|
'<button class="preset-delete-btn" onclick="LandingApp.deletePreset(' + JSON.stringify(preset.name) + ')" title="Delete preset">×<\/button>' +
|
|
'<\/div>';
|
|
}).join('');
|
|
}
|
|
|
|
// ── Actions ──────────────────────────────────────────────────────────────
|
|
|
|
function selectAll() {
|
|
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
|
|
cb.checked = true;
|
|
});
|
|
}
|
|
|
|
function selectNone() {
|
|
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
|
|
cb.checked = false;
|
|
});
|
|
}
|
|
|
|
function toggleProject(row) {
|
|
var cb = row.querySelector('input[type="checkbox"]');
|
|
if (cb) cb.checked = !cb.checked;
|
|
}
|
|
|
|
function openArchive() {
|
|
var checked = Array.from(document.querySelectorAll('#projectList input[type="checkbox"]:checked'))
|
|
.map(function(cb) { return cb.value; });
|
|
if (checked.length === 0) {
|
|
alert('Select at least one project to open.');
|
|
return;
|
|
}
|
|
var base = location.pathname.replace(/\/[^\/]*$/, '/');
|
|
var params = ['projects=' + checked.map(encodeURIComponent).join(',')];
|
|
// Propagate ?v= (channel selector) so the archive page loads through
|
|
// the same level-2 bootstrap stub on the same channel as this landing.
|
|
var v = new URLSearchParams(location.search).get('v');
|
|
if (v) {
|
|
params.push('v=' + encodeURIComponent(v));
|
|
}
|
|
location.href = base + 'archive.html?' + params.join('&');
|
|
}
|
|
|
|
function savePreset() {
|
|
var name = prompt('Enter a name for this preset:');
|
|
if (!name || !name.trim()) return;
|
|
name = name.trim();
|
|
var checked = Array.from(document.querySelectorAll('#projectList input[type="checkbox"]:checked'))
|
|
.map(function(cb) { return cb.value; });
|
|
// Replace existing preset with same name
|
|
presets = presets.filter(function(p) { return p.name !== name; });
|
|
presets.push({ name: name, projects: checked });
|
|
savePresets();
|
|
renderPresetMenu();
|
|
}
|
|
|
|
function togglePresetMenu() {
|
|
var menu = document.getElementById('presetMenu');
|
|
if (menu) menu.classList.toggle('hidden');
|
|
}
|
|
|
|
function applyPreset(name) {
|
|
var preset = presets.find(function(p) { return p.name === name; });
|
|
if (!preset) return;
|
|
var projectSet = new Set(preset.projects);
|
|
document.querySelectorAll('#projectList input[type="checkbox"]').forEach(function(cb) {
|
|
cb.checked = projectSet.has(cb.value);
|
|
});
|
|
document.getElementById('presetMenu').classList.add('hidden');
|
|
}
|
|
|
|
function deletePreset(name) {
|
|
presets = presets.filter(function(p) { return p.name !== name; });
|
|
savePresets();
|
|
renderPresetMenu();
|
|
}
|
|
|
|
function dismissWarning() {
|
|
var el = document.getElementById('accessWarningBanner');
|
|
if (el) el.classList.add('hidden');
|
|
}
|
|
|
|
// ── Warning ───────────────────────────────────────────────────────────────
|
|
|
|
function showWarning(message) {
|
|
var el = document.getElementById('accessWarningBanner');
|
|
var txt = document.getElementById('accessWarningText');
|
|
if (!el || !txt) return;
|
|
txt.textContent = message;
|
|
el.classList.remove('hidden');
|
|
}
|
|
|
|
// ── Persistence ───────────────────────────────────────────────────────────
|
|
|
|
function loadPresets() {
|
|
try {
|
|
var raw = localStorage.getItem(PRESETS_KEY);
|
|
presets = raw ? JSON.parse(raw) : [];
|
|
if (!Array.isArray(presets)) presets = [];
|
|
} catch (e) {
|
|
presets = [];
|
|
}
|
|
}
|
|
|
|
function savePresets() {
|
|
try {
|
|
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
|
|
} catch (e) { /* quota exceeded or private browsing */ }
|
|
}
|
|
|
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
|
|
function escapeHtml(text) {
|
|
var div = document.createElement('div');
|
|
div.textContent = String(text);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
window.LandingApp = {
|
|
init: init,
|
|
selectAll: selectAll,
|
|
selectNone: selectNone,
|
|
toggleProject: toggleProject,
|
|
openArchive: openArchive,
|
|
savePreset: savePreset,
|
|
togglePresetMenu: togglePresetMenu,
|
|
applyPreset: applyPreset,
|
|
deletePreset: deletePreset,
|
|
dismissWarning: dismissWarning
|
|
};
|
|
|
|
})();
|