QUELLCODE / JS
app.js
project-site/app.js
1/*2OpenAss: nur privat und experimentell. Bau, Download und Nutzung auf eigene Verantwortung.3Niemals Unternehmensrechner, Produktivsysteme, Industrieanlagen oder sicherheitskritische Systeme anschließen oder bedienen.4Keine freiwillige Funktions-/Sicherheitsgarantie; zwingende gesetzliche Ansprüche bleiben unberührt. Siehe DISCLAIMER.txt.5*/6/* The public project site is static. It does not connect to a hardware operator. */7(() => {8 'use strict';9 const $ = (selector, root = document) => root.querySelector(selector);10 const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];11 let toastTimer;12 function toast(message) {13 const node = $('#toast');14 if (!node) return;15 node.textContent = message;16 node.classList.add('show');17 clearTimeout(toastTimer);18 toastTimer = setTimeout(() => node.classList.remove('show'), 2400);19 }20 async function copy(value) {21 try {22 await navigator.clipboard.writeText(value);23 toast('Kopiert.');24 } catch {25 const area = document.createElement('textarea');26 area.value = value;27 area.style.position = 'fixed';28 area.style.opacity = '0';29 document.body.append(area);30 area.select();31 const worked = document.execCommand('copy');32 area.remove();33 toast(worked ? 'Kopiert.' : 'Kopieren nicht verfügbar. Bitte den Text manuell auswählen.');34 }35 }36 $$('.copy-command').forEach(button => button.addEventListener('click', () => {37 const command = document.getElementById(button.dataset.copyTarget);38 if (command) copy(command.textContent);39 }));40 $$('.checksum-button').forEach(button => button.addEventListener('click', () => copy(button.dataset.sha256)));41 const menu = $('.menu-toggle');42 const navigation = $('#main-nav');43 if (menu && navigation) {44 menu.addEventListener('click', () => {45 const expanded = menu.getAttribute('aria-expanded') !== 'true';46 menu.setAttribute('aria-expanded', String(expanded));47 menu.setAttribute('aria-label', expanded ? 'Navigation schließen' : 'Navigation öffnen');48 navigation.classList.toggle('open', expanded);49 });50 $$('a', navigation).forEach(link => link.addEventListener('click', () => {51 navigation.classList.remove('open');52 menu.setAttribute('aria-expanded', 'false');53 }));54 document.addEventListener('keydown', event => {55 if (event.key === 'Escape' && navigation.classList.contains('open')) {56 navigation.classList.remove('open');57 menu.setAttribute('aria-expanded', 'false');58 menu.focus();59 }60 });61 }6263 let color = 'ivory';64 let viewer = null;65 let viewerModule;66 let showViewer = false;67 const heroImage = $('#hero-product');68 const colorDescriptions = {69 ivory: 'Matte weiße LINK-Box mit gerundeten Ecken, kleinem roten Not-Aus-Pilz und USB-A-Kabel',70 anthracite: 'Matte anthrazitfarbene LINK-Box mit kleinem roten Not-Aus-Pilz und USB-A-Kabel'71 };72 function applyModelColor() {73 if (!viewer?.model) return;74 const shell = viewer.model.materials.find(material => material.name.startsWith('Warm chalk /'));75 if (shell) shell.pbrMetallicRoughness.setBaseColorFactor(color === 'ivory' ? [.72, .695, .64, 1] : [.035, .042, .044, 1]);76 }77 $$('.color-button').forEach(button => button.addEventListener('click', () => {78 color = button.dataset.color;79 $$('.color-button').forEach(item => {80 const active = item === button;81 item.classList.toggle('active', active);82 item.setAttribute('aria-pressed', String(active));83 });84 if (heroImage) {85 heroImage.src = `assets/${color}.webp`;86 heroImage.alt = colorDescriptions[color];87 }88 applyModelColor();89 }));90 const viewerButton = $('#viewer-button');91 const viewerContainer = $('#viewer-container');92 const viewerStatus = $('#viewer-status');93 if (viewerButton && viewerContainer) {94 viewerButton.addEventListener('click', async () => {95 if (showViewer) {96 showViewer = false;97 viewerContainer.hidden = true;98 viewerButton.querySelector('span').textContent = '3D ansehen';99 viewerStatus.textContent = '';100 return;101 }102 viewerButton.disabled = true;103 viewerStatus.textContent = '3D-Modell wird geladen …';104 try {105 viewerModule ??= import('./assets/vendor/model-viewer.min.js');106 await viewerModule;107 if (!viewer) {108 viewer = document.createElement('model-viewer');109 viewer.setAttribute('src', 'downloads/LINK-print-edition.glb');110 viewer.setAttribute('alt', 'Drehbares 3D-Modell der LINK Hardware Box mit USB-Kabel und Mini-Not-Aus');111 viewer.setAttribute('camera-controls', '');112 viewer.setAttribute('touch-action', 'pan-y');113 viewer.setAttribute('shadow-intensity', '1');114 viewer.setAttribute('exposure', '1');115 viewer.setAttribute('camera-orbit', '40deg 65deg auto');116 viewer.setAttribute('camera-target', '0.02m 0.025m 0.005m');117 viewer.setAttribute('interaction-prompt', 'none');118 viewer.addEventListener('load', () => {119 applyModelColor();120 viewerStatus.textContent = 'Ziehen zum Drehen · Scrollen oder zwei Finger zum Zoomen';121 });122 viewer.addEventListener('error', () => {123 viewerStatus.textContent = '3D ist in diesem Browser nicht verfügbar. Das GLB-Modell findest du bei den Downloads.';124 viewerContainer.hidden = true;125 showViewer = false;126 viewerButton.querySelector('span').textContent = '3D ansehen';127 });128 viewerContainer.append(viewer);129 } else {130 viewerStatus.textContent = 'Ziehen zum Drehen · Scrollen oder zwei Finger zum Zoomen';131 }132 viewerContainer.hidden = false;133 showViewer = true;134 viewerButton.querySelector('span').textContent = 'Foto ansehen';135 } catch {136 viewerStatus.textContent = 'Der 3D-Viewer konnte nicht geladen werden. Das Modell steht als GLB zum Download bereit.';137 viewerModule = null;138 } finally {139 viewerButton.disabled = false;140 }141 });142 }143144 const viewInfo = {145 internals: ['Innenaufbau mit Raspberry Pi 5, Active Cooler, Pico 2, Capture-Modul und Kabeln', 'Modellierte Elektronik und Kabelführung. Die endgültige Hardwarepassung wird am Prototyp geprüft.'],146 rear: ['Rückseite der LINK-Box mit USB-Kabel, USB-C, LAN, HDMI OUT und HDMI IN', 'Ein gemeinsamer Rückseiteneinsatz bündelt HDMI IN / OUT, LAN, Versorgung und das USB-Zielkabel.'],147 open: ['Explosionsansicht der LINK-Box mit angehobener Oberschale und sichtbarer Elektronik', 'Oberschale, Bodenwanne und Rückseiteneinsatz sind getrennte Druckteile. Der Innenaufbau lässt sich in Blender bearbeiten.']148 };149 $$('.gallery-tabs button').forEach(button => button.addEventListener('click', () => {150 const view = button.dataset.view;151 const image = $('#gallery-image');152 image.src = `assets/${view}.webp`;153 image.alt = viewInfo[view][0];154 $('#gallery-caption').textContent = viewInfo[view][1];155 $$('.gallery-tabs button').forEach(item => {156 item.classList.toggle('active', item === button);157 item.setAttribute('aria-pressed', String(item === button));158 });159 }));160 $$('[data-flow]').forEach(button => button.addEventListener('click', () => {161 const control = button.dataset.flow === 'control';162 $('#signal-diagram').classList.toggle('control-active', control);163 $('#flow-caption').textContent = control164 ? 'Astra schlägt eine Aktion vor. Nach lokaler Prüfung und deiner Freigabe sendet der Pi sie über isolierte UART an den Pico. Der Pico bedient den Zielcomputer per USB-HID.'165 : 'Der HDMI-Loop gibt das Bild direkt an deinen Monitor weiter. Parallel empfängt der Pi das Capture-Bild über USB.';166 $$('[data-flow]').forEach(item => {167 item.classList.toggle('active', item === button);168 item.setAttribute('aria-pressed', String(item === button));169 });170 }));171 let filter = 'all';172 const cards = $$('.download-card');173 const search = $('#download-search');174 const groups = { design: ['design', 'print', 'history'], docs: ['docs', 'integrity'] };175 function filterDownloads() {176 const query = (search?.value || '').toLocaleLowerCase('de').trim();177 let count = 0;178 for (const card of cards) {179 const groupMatch = filter === 'all' || (groups[filter] || [filter]).includes(card.dataset.group);180 const textMatch = card.textContent.toLocaleLowerCase('de').includes(query) || card.dataset.filename.toLocaleLowerCase().includes(query);181 card.hidden = !(groupMatch && textMatch);182 if (!card.hidden) count++;183 }184 if ($('#download-count')) $('#download-count').textContent = `${count} ${count === 1 ? 'Datei / Paket' : 'Dateien / Pakete'}`;185 if ($('#download-empty')) $('#download-empty').hidden = count !== 0;186 }187 $$('[data-group]').forEach(button => button.addEventListener('click', () => {188 filter = button.dataset.group;189 $$('[data-group]').forEach(item => {190 item.classList.toggle('active', item === button);191 item.setAttribute('aria-pressed', String(item === button));192 });193 filterDownloads();194 }));195 search?.addEventListener('input', filterDownloads);196 if (cards.length) filterDownloads();197 const sourceSearch = $('#source-search');198 if (sourceSearch) sourceSearch.addEventListener('input', () => {199 const query = sourceSearch.value.toLocaleLowerCase();200 let count = 0;201 $$('.source-item').forEach(item => {202 item.hidden = !item.dataset.path.toLocaleLowerCase().includes(query);203 if (!item.hidden) count++;204 });205 $('#source-count').textContent = `${count} Quelldateien`;206 });207})();