From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally
---
packages/bibliothek-client/src/components/bibliothek-zeile.ts | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 235 insertions(+), 0 deletions(-)
diff --git a/packages/bibliothek-client/src/components/bibliothek-zeile.ts b/packages/bibliothek-client/src/components/bibliothek-zeile.ts
new file mode 100644
index 0000000..5368285
--- /dev/null
+++ b/packages/bibliothek-client/src/components/bibliothek-zeile.ts
@@ -0,0 +1,235 @@
+import { LitElement, html, nothing } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { event, type EventRef } from '@dh-software/lit-extensions';
+import type { Software } from 'bibliothek-types';
+
+/**
+ * Eine kompakte Katalog-Zeile für einen Software-Eintrag. Feuert Aktions-Events an die App.
+ * Die Aktionen liegen in festen Spalten, damit sie über alle Zeilen hinweg untereinander stehen;
+ * fehlt eine Aktion, bleibt ihre Spalte leer.
+ *
+ * Zusatzinhalte (Plug-ins, Sprachpakete, Vorlagen …) stehen eingerückt direkt unter ihrem Tool
+ * und bilden mit ihm eine Gruppe.
+ */
+@customElement('bibliothek-zeile')
+export class BibliothekZeile extends LitElement {
+ @property({ attribute: false }) software!: Software;
+ /** Weitere Einträge, die zu dieser Software gehören. */
+ @property({ attribute: false }) zusatzinhalte: Software[] = [];
+ @property({ type: Boolean }) lokal = false;
+ /** Zeile eines Zusatzinhalts (eingerückt unter seinem Tool). */
+ @property({ type: Boolean }) eingerueckt = false;
+ /** Ist dieser Eintrag für den Sammel-Download angehakt? */
+ @property({ type: Boolean }) gewaehlt = false;
+ /** Ids aller angehakten Einträge — nötig, damit auch eingerückte Zeilen ihren Zustand kennen. */
+ @property({ attribute: false }) auswahl: ReadonlySet<number> = new Set();
+
+ @event({ bubbles: true, composed: true }, 'auswahlGeaendert')
+ private auswahlGeaendert!: EventRef<{ software: Software; gewaehlt: boolean }>;
+
+ @event({ bubbles: true, composed: true }, 'herunterladen')
+ private herunterladen!: EventRef<Software>;
+
+ @event({ bubbles: true, composed: true }, 'pruefen')
+ private pruefen!: EventRef<Software>;
+
+ @event({ bubbles: true, composed: true }, 'bearbeiten')
+ private bearbeiten!: EventRef<Software>;
+
+ @event({ bubbles: true, composed: true }, 'loeschen')
+ private loeschen!: EventRef<Software>;
+
+ // Light DOM, damit das globale dh-components-Theme greift.
+ createRenderRoot() {
+ return this;
+ }
+
+ render() {
+ const software = this.software;
+ return html`
+ <div class="zeile ${this.eingerueckt ? 'zeile-eingerueckt' : ''}">
+ <span class="zeile-auswahl">
+ <dh-checkbox
+ ?checked=${this.gewaehlt}
+ ?disabled=${!software.dateiname}
+ title=${software.dateiname ? 'Für Sammel-Download auswählen' : 'Keine Datei vorhanden'}
+ @isChange=${this.beiAuswahl}
+ ></dh-checkbox>
+ </span>
+ <div class="zeile-info">
+ <span class="zeile-name">
+ ${software.name}
+ ${software.einstufung === 'lizenzpflichtig'
+ ? html`<span class="lizenz-marke" title="Lizenzschlüssel über den Support beziehen">lizenzpflichtig</span>`
+ : nothing}
+ ${software.einstufung === 'intern' ? html`<span class="intern-marke">intern</span>` : nothing}
+ </span>
+ <span class="zeile-meta">
+ ${this.rendereStand()}
+ <span class="${this.statusKlasse()}">${this.statusText()}</span>
+ ${this.rendereSignatur()}
+ </span>
+ ${this.rendereHinweise()}
+ </div>
+ <div class="zeile-aktionen">
+ <span class="aktion">
+ ${software.anleitungUrl
+ ? html`<a class="anleitung" href=${software.anleitungUrl} target="_blank" rel="noopener noreferrer">Anleitung</a>`
+ : nothing}
+ </span>
+ <span class="aktion">
+ ${software.dateiname
+ ? html`<dh-button
+ icon-button
+ variant="filled"
+ title=${`${software.name} herunterladen`}
+ @click=${this.beiHerunterladen}
+ >
+ <span slot="img" class="fv-icon-download"></span>
+ </dh-button>`
+ : nothing}
+ </span>
+ ${this.lokal
+ ? html`
+ <span class="aktion">
+ ${software.quelleTyp && software.quelleTyp !== 'manuell'
+ ? html`<dh-button buttonText="Prüfen" variant="standard" @click=${this.beiPruefen}></dh-button>`
+ : nothing}
+ </span>
+ <span class="aktion">
+ <dh-button buttonText="Bearbeiten" variant="standard" @click=${this.beiBearbeiten}></dh-button>
+ </span>
+ <span class="aktion">
+ <dh-button buttonText="Löschen" variant="standard" @click=${this.beiLoeschen}></dh-button>
+ </span>
+ `
+ : nothing}
+ </div>
+ </div>
+ ${this.zusatzinhalte.map(
+ (zusatzinhalt) => html`<bibliothek-zeile
+ .software=${zusatzinhalt}
+ .auswahl=${this.auswahl}
+ ?gewaehlt=${this.auswahl.has(zusatzinhalt.id)}
+ ?lokal=${this.lokal}
+ eingerueckt
+ ></bibliothek-zeile>`
+ )}
+ `;
+ }
+
+ private beiAuswahl(ereignis: CustomEvent<boolean>): void {
+ this.auswahlGeaendert.dispatch({ software: this.software, gewaehlt: ereignis.detail });
+ }
+
+ /**
+ * Hinweise zum Umgang mit der Datei: was aus der Programmart folgt, plus der
+ * freie Hinweis am Eintrag. Die eigene Zeile kostet nur dort Höhe, wo wirklich
+ * etwas zu sagen ist — die meisten Einträge bleiben einzeilig.
+ */
+ private rendereHinweise() {
+ const software = this.software;
+ const teile: string[] = [];
+ if (software.dateiname && software.programmart === 'installer') {
+ teile.push('Datei kann nach der Installation gelöscht werden');
+ }
+ if (software.dateiname && software.programmart === 'eigenstaendig') {
+ teile.push('Läuft direkt — an einen festen Ort legen');
+ }
+ if (software.hinweis) {
+ teile.push(software.hinweis);
+ }
+ return teile.length > 0 ? html`<span class="zeile-hinweis">${teile.join(' · ')}</span>` : nothing;
+ }
+
+ /**
+ * Signaturbefund der bereitgestellten Datei — nur in der Verwaltung.
+ * Für die Netzwerk-Ansicht ist das Betriebswissen ohne Nutzen: Wer etwas
+ * herunterlädt, kann daraus keine Entscheidung ableiten.
+ */
+ private rendereSignatur() {
+ const software = this.software;
+ if (!this.lokal || !software.dateiname || !software.signaturStatus) {
+ return nothing;
+ }
+ if (software.signaturStatus === 'Valid') {
+ return html` ·
+ <span class="signatur-gut" title="Gültig signiert von ${software.signaturHerausgeber}">
+ signiert: ${software.signaturHerausgeber}
+ </span>`;
+ }
+ const text = software.signaturStatus === 'NotSigned' ? 'unsigniert' : `Signatur unklar (${software.signaturStatus})`;
+ return html` · <span class="signatur-unklar" title="Windows-Signaturstatus: ${software.signaturStatus}">${text}</span>`;
+ }
+
+ /**
+ * Zeigt bevorzugt das Veröffentlichungsdatum des Anbieters — das ist die Angabe,
+ * die den Nutzer interessiert. Nur wenn die Quelle keins liefert, wird ersatzweise
+ * unser Abrufzeitpunkt genannt (und als solcher benannt).
+ */
+ private rendereStand() {
+ const software = this.software;
+ if (software.veroeffentlichtAm) {
+ return html`Stand ${this.alsDatum(software.veroeffentlichtAm)} · `;
+ }
+ if (software.dateiStandVom) {
+ return html`geholt ${this.alsDatum(software.dateiStandVom)} · `;
+ }
+ return nothing;
+ }
+
+ private alsDatum(zeitstempel: string): string {
+ const zeitpunkt = new Date(zeitstempel);
+ return Number.isNaN(zeitpunkt.getTime()) ? '—' : zeitpunkt.toLocaleDateString('de-DE');
+ }
+
+ private statusText(): string {
+ const software = this.software;
+ if (software.dateiname) {
+ return software.dateiname;
+ }
+ if (!software.quelleTyp) {
+ return 'keine Quelle';
+ }
+ if (software.quelleTyp === 'manuell') {
+ return 'keine Datei hinterlegt';
+ }
+ if (software.letzterAbgleich && !software.abgleichErfolg) {
+ return software.abgleichMeldung || 'Abgleich fehlgeschlagen';
+ }
+ return 'Quelle hinterlegt, noch nicht geladen';
+ }
+
+ private statusKlasse(): string {
+ const software = this.software;
+ if (software.dateiname) {
+ return 'status-ok';
+ }
+ if (!software.quelleTyp || software.quelleTyp === 'manuell' || (software.letzterAbgleich && !software.abgleichErfolg)) {
+ return 'status-fehler';
+ }
+ return 'status-ausstehend';
+ }
+
+ private beiHerunterladen(): void {
+ this.herunterladen.dispatch(this.software);
+ }
+
+ private beiPruefen(): void {
+ this.pruefen.dispatch(this.software);
+ }
+
+ private beiBearbeiten(): void {
+ this.bearbeiten.dispatch(this.software);
+ }
+
+ private beiLoeschen(): void {
+ this.loeschen.dispatch(this.software);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'bibliothek-zeile': BibliothekZeile;
+ }
+}
--
Gitblit v1.9.3