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})();