Compare commits
2
Commits
532473b1f9
...
2bd13f0f19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd13f0f19 | ||
|
|
144470a413 |
+10
-9
@@ -149,11 +149,16 @@ export type Bibliothek = {
|
||||
extraVersionCount: number;
|
||||
};
|
||||
export type Seite<T> = { items: T[]; total: number };
|
||||
export type Lieferung = {
|
||||
method: 'directFile' | 'remux'; reasons: string[];
|
||||
audioToEncode: number[]; subtitlesDropped: number[];
|
||||
packageState: string; progress: number; error: string | null;
|
||||
};
|
||||
|
||||
/* **Herunterladen gibt es im Browser nicht mehr.**
|
||||
*
|
||||
* Hier standen `Lieferung`, `lieferung()` und `downloadAdresse()`. Die
|
||||
* Detailseite bot damit einen Knopf an, der die Datei als MP4 herausgab —
|
||||
* und das ist etwas anderes, als einen Strom zu zeigen: wer eine Datei
|
||||
* bekommt, hat sie und kann sie weitergeben. Der Endpunkt bleibt, weil die
|
||||
* Apps ihn für die Mitnahme aufs Gerät brauchen; die Weboberfläche bietet
|
||||
* ihn nicht mehr an. */
|
||||
|
||||
export type Scanstand = {
|
||||
state: string; filesFound: number | null; added: number | null;
|
||||
removed: number | null; unrecognised: number | null; probed: number | null;
|
||||
@@ -561,8 +566,6 @@ export const folgen = (serie: string, staffel?: number) =>
|
||||
export const film = (id: string) => anfrage<Film>(`/movies/${id}`);
|
||||
export const serie = (id: string) => anfrage<Serie>(`/series/${id}`);
|
||||
|
||||
export const lieferung = (datei: string) => anfrage<Lieferung>(`/files/${datei}/delivery`);
|
||||
|
||||
/* ---------- Stand je Benutzer ---------- */
|
||||
|
||||
export const weiterschauen = (take = 20) =>
|
||||
@@ -614,8 +617,6 @@ export const favorit = (id: string, wert: boolean) =>
|
||||
method: 'PUT', body: JSON.stringify({ favourite: wert }),
|
||||
});
|
||||
|
||||
export const downloadAdresse = (datei: string) => `/api/v1/files/${datei}/download`;
|
||||
|
||||
/* ---------- Strom ---------- */
|
||||
|
||||
/** Eine wählbare Spur, so wie sie im Menü steht. */
|
||||
|
||||
@@ -838,6 +838,26 @@ textarea.feld {
|
||||
}
|
||||
.pl-video { width: 100%; height: 100%; object-fit: contain; background: #000; }
|
||||
|
||||
/* **Das Bild füllt die Fläche, dann sitzt es auch mittig.**
|
||||
*
|
||||
* Gemessen an einem 21:9-Film auf einem 1024×768-Fenster: die Hülle war
|
||||
* 768 hoch, der Videokasten 459 — und er klebte bei oben 0. Die Breite
|
||||
* stimmte, die Höhe nicht, und der schwarze Rand lag ganz unten statt
|
||||
* gleichmässig oben und unten. Im Fenster fällt das kaum auf, im Vollbild
|
||||
* sofort.
|
||||
*
|
||||
* **Woran es lag.** Die Hülle ist ein Raster mit `place-items: center`.
|
||||
* Damit wird das Videoelement nicht gedehnt, und sein `height: 100%` löst
|
||||
* sich gegen eine Zeile auf, die ihre Höhe wiederum aus dem Element nimmt —
|
||||
* die Prozentangabe hatte nichts, woran sie sich halten konnte.
|
||||
*
|
||||
* Absolut über die ganze Hülle nimmt die Frage weg: `object-fit: contain`
|
||||
* rechnet dann gegen den vollen Bildschirm und legt den schwarzen Rand von
|
||||
* selbst zu gleichen Teilen oben und unten. Die kleine Leiste positioniert
|
||||
* dasselbe Element ohnehin absolut und bleibt unberührt — deshalb das
|
||||
* `:not(.pl-klein)`. */
|
||||
.pl:not(.pl-klein) .pl-video { position: absolute; inset: 0; }
|
||||
|
||||
.pl-dreher { position: absolute; margin: 0; }
|
||||
|
||||
/* Kopf und Leiste blenden sich weg, während etwas läuft. */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiFehler,
|
||||
film as filmLaden, serie as serieLaden, folgen, lieferung, downloadAdresse,
|
||||
film as filmLaden, serie as serieLaden, folgen,
|
||||
vorschlaege, zuordnen, bildAdresse, istAdmin, gesehen, favorit, weglegen,
|
||||
type Film, type Serie, type Folge, type Datei, type Lieferung, type Stand,
|
||||
type Film, type Serie, type Folge, type Datei, type Stand,
|
||||
type Mitwirkender, type Spur, aehnliche,
|
||||
} from '../api';
|
||||
import {
|
||||
@@ -313,10 +313,12 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
const tonwahl = tonspurwahl(datei);
|
||||
if (tonwahl) reihe.appendChild(tonwahl);
|
||||
|
||||
const laden = knopf('Herunterladen', 'btn');
|
||||
laden.prepend(icon('download'));
|
||||
laden.onclick = () => void herunterladen(datei, laden, hinweisfeld);
|
||||
reihe.appendChild(laden);
|
||||
// **Kein „Herunterladen" im Browser.**
|
||||
//
|
||||
// Hier stand ein Knopf, der die Datei als MP4 herausgab. Das ist etwas
|
||||
// anderes, als einen Strom zu zeigen: wer eine Datei bekommt, hat sie
|
||||
// und kann sie weitergeben. Die Apps holen sich eine Mitnahme
|
||||
// weiterhin über `/download` — dort ist sie an ein Gerät gebunden.
|
||||
}
|
||||
|
||||
// **Runde Knöpfe ohne Beschriftung, wie bei Plex.** Gesehen, Favorit,
|
||||
@@ -743,41 +745,7 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
return block;
|
||||
}
|
||||
|
||||
/* ---------- Abspielen und Herunterladen ---------- */
|
||||
|
||||
/** Sorgt dafür, dass die Datei abholbereit ist — packt um, falls nötig. */
|
||||
/** Baut die Datei für den **Download** auf — dort ist eine fertige richtig.
|
||||
*
|
||||
* **Nicht mehr fürs Abspielen.** Dafür gibt es den Strom, der sofort
|
||||
* anfängt; hier auf eine vollständige MP4 zu warten hiess bei einem
|
||||
* 4K-Film: Minuten, in denen sichtbar nichts geschieht. */
|
||||
async function bereitstellen(datei: Datei, hinweisfeld: HTMLElement): Promise<boolean> {
|
||||
let auskunft: Lieferung;
|
||||
try { auskunft = await lieferung(datei.id); }
|
||||
catch (fehler) { hinweisfeld.textContent = meldung(fehler); return false; }
|
||||
|
||||
if (auskunft.method === 'directFile') return true;
|
||||
if (auskunft.packageState === 'ready') return true;
|
||||
|
||||
// **Hier steht, was passieren wird, bevor es passiert.**
|
||||
const gruende: string[] = [];
|
||||
if (auskunft.audioToEncode.length) gruende.push(`${auskunft.audioToEncode.length} Tonspur(en) werden neu gerechnet`);
|
||||
if (auskunft.subtitlesDropped.length) gruende.push(`${auskunft.subtitlesDropped.length} Bilduntertitel können nicht mit`);
|
||||
hinweisfeld.textContent = `Wird in eine MP4 umgepackt${gruende.length ? ` — ${gruende.join(', ')}` : ''}…`;
|
||||
|
||||
for (let i = 0; i < 600; i++) {
|
||||
const antwort = await fetch(downloadAdresse(datei.id), { method: 'HEAD' });
|
||||
if (antwort.ok) { hinweisfeld.textContent = ''; return true; }
|
||||
if (antwort.status !== 202) { hinweisfeld.textContent = `Umpacken gescheitert (${antwort.status}).`; return false; }
|
||||
|
||||
const wie = await lieferung(datei.id);
|
||||
if (wie.error) { hinweisfeld.textContent = wie.error; return false; }
|
||||
hinweisfeld.textContent = `Wird umgepackt… ${Math.round(wie.progress * 100)} %`;
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
hinweisfeld.textContent = 'Das Umpacken dauert ungewöhnlich lange.';
|
||||
return false;
|
||||
}
|
||||
/* ---------- Abspielen ---------- */
|
||||
|
||||
/** Startet die Wiedergabe — ohne Vorlauf, ohne Warten.
|
||||
*
|
||||
@@ -796,21 +764,6 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem
|
||||
});
|
||||
}
|
||||
|
||||
async function herunterladen(
|
||||
datei: Datei, knopfDavon: HTMLButtonElement, hinweisfeld: HTMLElement,
|
||||
): Promise<void> {
|
||||
knopfDavon.disabled = true;
|
||||
if (await bereitstellen(datei, hinweisfeld)) {
|
||||
// Ein echter Link, kein Blob: der Browser lädt im Hintergrund und kann
|
||||
// fortsetzen, statt den ganzen Film in den Speicher zu nehmen.
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadAdresse(datei.id);
|
||||
a.download = '';
|
||||
a.click();
|
||||
}
|
||||
knopfDavon.disabled = false;
|
||||
}
|
||||
|
||||
/* ---------- Zuordnung korrigieren ---------- */
|
||||
|
||||
function zuordnungsbereich(titel: string, jahr: number | null): HTMLElement {
|
||||
|
||||
Reference in New Issue
Block a user