import { LitElement, html, nothing } from 'lit';
|
import { customElement, state } from 'lit/decorators.js';
|
import { EINSTUFUNGEN, type Einstufung } from 'bibliothek-types';
|
import { bruecke, type EintragMitBefund, type Einstellungen } from '../bruecke';
|
import { gemerkterModus, setzeTheme, type ThemeModus } from '../theme';
|
import './client-zeile';
|
|
interface Auswahloption {
|
dataId: string;
|
showContent: string;
|
selected?: boolean;
|
}
|
|
const THEME_OPTIONEN: Array<{ id: ThemeModus; text: string }> = [
|
{ id: 'system', text: 'System' },
|
{ id: 'hell', text: 'Hell' },
|
{ id: 'dunkel', text: 'Dunkel' },
|
];
|
|
/** Wurzel-Komponente des Clients. */
|
@customElement('client-app')
|
export class ClientApp extends LitElement {
|
@state() private eintraege: EintragMitBefund[] = [];
|
@state() private aktiveEinstufung: Einstufung = 'lizenzfrei';
|
@state() private suchbegriff = '';
|
@state() private ladefehler = '';
|
@state() private meldung = '';
|
@state() private laeuftFuer = 0;
|
@state() private einstellungenOffen = false;
|
@state() private einstellungen?: Einstellungen;
|
@state() private serverAdresse = '';
|
@state() private werkzeugOrdner = '';
|
@state() private themeOptionen: Auswahloption[] = [];
|
|
// Light DOM, damit das globale dh-components-Theme greift.
|
createRenderRoot() {
|
return this;
|
}
|
|
connectedCallback(): void {
|
super.connectedCallback();
|
const modus = gemerkterModus();
|
this.themeOptionen = THEME_OPTIONEN.map((eintrag) => ({
|
dataId: eintrag.id,
|
showContent: eintrag.text,
|
selected: eintrag.id === modus,
|
}));
|
void this.alesLaden();
|
}
|
|
render() {
|
return html`
|
<div class="rahmen">
|
<header class="kopf">
|
<h1>Software-Bibliothek</h1>
|
<div class="kopf-aktionen">
|
<dh-select
|
class="theme-select"
|
withoutEmptyDefault
|
notSearchable
|
.selectOptions=${this.themeOptionen}
|
@changeSelectOption=${this.beiTheme}
|
></dh-select>
|
<dh-button icon-button variant="standard" title="Werkzeug-Ordner öffnen" @click=${this.beiOrdnerOeffnen}>
|
<span slot="img" class="fv-icon-folder"></span>
|
</dh-button>
|
<dh-button icon-button variant="standard" title="Einstellungen" @click=${this.beiEinstellungenOeffnen}>
|
<span slot="img" class="fv-icon-settings"></span>
|
</dh-button>
|
</div>
|
</header>
|
|
${this.ladefehler ? html`<p class="warnung">${this.ladefehler}</p>` : nothing}
|
${this.meldung ? html`<p class="hinweis">${this.meldung}</p>` : nothing}
|
${this.rendereUpdateHinweis()}
|
${this.einstellungenOffen ? this.rendereEinstellungen() : this.rendereKatalog()}
|
</div>
|
`;
|
}
|
|
// --- Teilansichten ---
|
|
/** Der Grund, warum es diesen Client gibt: ungefragt sagen, was veraltet ist. */
|
private rendereUpdateHinweis() {
|
const veraltet = this.eintraege.filter(
|
(eintrag) => eintrag.ueberwacht && (eintrag.lokal?.zustand === 'veraltet' || eintrag.lokal?.zustand === 'fremd')
|
);
|
if (veraltet.length === 0) {
|
return nothing;
|
}
|
return html`<p class="update-hinweis">
|
${veraltet.length === 1 ? 'Ein Werkzeug ist nicht auf dem neuesten Stand' : `${veraltet.length} Werkzeuge sind nicht auf dem neuesten Stand`}:
|
${veraltet.map((eintrag) => eintrag.name).join(', ')}.
|
</p>`;
|
}
|
|
private rendereKatalog() {
|
if (this.eintraege.length === 0) {
|
return html`<p class="leer">${this.ladefehler ? '' : 'Katalog wird geladen …'}</p>`;
|
}
|
return html`
|
<dh-input class="suche" type="text" placeholder="Suchen" .value=${this.suchbegriff} @input=${this.beiSuche}></dh-input>
|
<dh-tab stretch @clickTab=${this.beiReiterWechsel}>
|
${EINSTUFUNGEN.map(
|
(eintrag) => html`<div
|
slot="tab-head"
|
content=${eintrag.id}
|
class=${eintrag.id === this.aktiveEinstufung ? 'selected' : ''}
|
>
|
${eintrag.text} (${this.mitEinstufung(eintrag.id).length})
|
</div>`
|
)}
|
${EINSTUFUNGEN.map(
|
(eintrag) => html`<div
|
slot="tab-content"
|
tab=${eintrag.id}
|
class=${eintrag.id === this.aktiveEinstufung ? '' : 'hidden'}
|
>
|
${eintrag.id === this.aktiveEinstufung ? this.rendereListe() : nothing}
|
</div>`
|
)}
|
</dh-tab>
|
`;
|
}
|
|
private rendereListe() {
|
const gefiltert = this.gefiltert();
|
if (gefiltert.length === 0) {
|
return html`<p class="leer">
|
${this.suchbegriff ? html`Keine Treffer für „${this.suchbegriff}".` : 'In dieser Einstufung ist nichts hinterlegt.'}
|
</p>`;
|
}
|
return html`<div class="liste">
|
${gefiltert.map(
|
(eintrag) => html`<client-zeile
|
.eintrag=${eintrag}
|
?laeuft=${this.laeuftFuer === eintrag.id}
|
@holen=${this.beiHolen}
|
@verknuepfen=${this.beiVerknuepfen}
|
></client-zeile>`
|
)}
|
</div>`;
|
}
|
|
private rendereEinstellungen() {
|
return html`
|
<dh-card variant="outlined" no-hover-effect no-click-effect>
|
<span slot="header">Einstellungen</span>
|
<dh-form-grid slot="content">
|
<dh-form-field label="Adresse der Bibliothek">
|
<dh-input type="text" .value=${this.serverAdresse} @input=${this.beiServerAdresse}></dh-input>
|
<span class="feldhinweis">
|
Rechnername statt IP-Adresse ist die bessere Wahl — er überlebt einen Wechsel der Adresse.
|
</span>
|
</dh-form-field>
|
<dh-form-field label="Werkzeug-Ordner">
|
<dh-input type="text" .value=${this.werkzeugOrdner} @input=${this.beiWerkzeugOrdner}></dh-input>
|
<span class="feldhinweis">
|
Hierhin kommen eigenständige Programme; Installationsprogramme in den Unterordner
|
„Installationsprogramme".
|
</span>
|
</dh-form-field>
|
</dh-form-grid>
|
<div slot="actions">
|
<dh-button buttonText="Speichern" variant="filled" @click=${this.beiEinstellungenSpeichern}></dh-button>
|
<dh-button buttonText="Abbrechen" variant="standard" @click=${this.beiEinstellungenSchliessen}></dh-button>
|
</div>
|
</dh-card>
|
`;
|
}
|
|
// --- Daten ---
|
|
private async alesLaden(): Promise<void> {
|
const api = await bruecke();
|
const stand = await api.einstellungen();
|
this.einstellungen = stand.einstellungen;
|
this.serverAdresse = stand.einstellungen.serverAdresse;
|
this.werkzeugOrdner = stand.einstellungen.werkzeugOrdner;
|
await this.katalogLaden();
|
}
|
|
private async katalogLaden(): Promise<void> {
|
const api = await bruecke();
|
const antwort = await api.katalog();
|
if (!antwort.ok) {
|
this.ladefehler = `${antwort.meldung} — Adresse in den Einstellungen prüfen.`;
|
this.eintraege = [];
|
return;
|
}
|
this.ladefehler = '';
|
this.eintraege = antwort.eintraege;
|
}
|
|
private mitEinstufung(einstufung: Einstufung): EintragMitBefund[] {
|
return this.eintraege.filter((eintrag) => eintrag.einstufung === einstufung && !eintrag.elternId);
|
}
|
|
private gefiltert(): EintragMitBefund[] {
|
const suche = this.suchbegriff.trim().toLowerCase();
|
const inDerEinstufung = this.mitEinstufung(this.aktiveEinstufung);
|
if (!suche) {
|
return inDerEinstufung;
|
}
|
return inDerEinstufung.filter(
|
(eintrag) =>
|
eintrag.name.toLowerCase().includes(suche) || eintrag.beschreibung.toLowerCase().includes(suche)
|
);
|
}
|
|
// --- Ereignisse ---
|
|
private beiTheme(ereignis: CustomEvent<{ selectedOption: { dataId: string } }>): void {
|
setzeTheme(ereignis.detail.selectedOption.dataId as ThemeModus);
|
}
|
|
private beiSuche(ereignis: CustomEvent<string>): void {
|
this.suchbegriff = ereignis.detail;
|
}
|
|
/** dh-tab liefert die Kennung des Reiters unmittelbar als `detail`. */
|
private beiReiterWechsel(ereignis: CustomEvent<string>): void {
|
this.aktiveEinstufung = ereignis.detail as Einstufung;
|
}
|
|
private async beiHolen(ereignis: CustomEvent<EintragMitBefund>): Promise<void> {
|
const eintrag = ereignis.detail;
|
this.laeuftFuer = eintrag.id;
|
this.meldung = `${eintrag.name} wird geholt …`;
|
const api = await bruecke();
|
const ergebnis = await api.herunterladen(eintrag.id, eintrag.dateiname, eintrag.version, eintrag.programmart);
|
this.meldung = ergebnis.meldung;
|
this.laeuftFuer = 0;
|
await this.katalogLaden();
|
}
|
|
private async beiVerknuepfen(ereignis: CustomEvent<EintragMitBefund>): Promise<void> {
|
const eintrag = ereignis.detail;
|
if (!eintrag.lokal?.pfad) {
|
return;
|
}
|
const api = await bruecke();
|
const ergebnis = await api.verknuepfung_anlegen(eintrag.lokal.pfad, eintrag.name);
|
this.meldung = ergebnis.meldung ?? '';
|
}
|
|
private async beiOrdnerOeffnen(): Promise<void> {
|
const api = await bruecke();
|
await api.ordner_oeffnen();
|
}
|
|
private beiEinstellungenOeffnen(): void {
|
this.serverAdresse = this.einstellungen?.serverAdresse ?? '';
|
this.werkzeugOrdner = this.einstellungen?.werkzeugOrdner ?? '';
|
this.einstellungenOffen = true;
|
}
|
|
private beiEinstellungenSchliessen(): void {
|
this.einstellungenOffen = false;
|
}
|
|
private beiServerAdresse(ereignis: CustomEvent<string>): void {
|
this.serverAdresse = ereignis.detail;
|
}
|
|
private beiWerkzeugOrdner(ereignis: CustomEvent<string>): void {
|
this.werkzeugOrdner = ereignis.detail;
|
}
|
|
private async beiEinstellungenSpeichern(): Promise<void> {
|
const api = await bruecke();
|
const ergebnis = await api.einstellungen_speichern(this.serverAdresse, this.werkzeugOrdner);
|
this.einstellungen = ergebnis.einstellungen;
|
this.einstellungenOffen = false;
|
this.meldung = 'Einstellungen gespeichert.';
|
await this.katalogLaden();
|
}
|
}
|
|
declare global {
|
interface HTMLElementTagNameMap {
|
'client-app': ClientApp;
|
}
|
}
|