Die Seite hiess „Verwaltung" und war für alle anderen gar nicht erreichbar — „Meine Sprache" lag dadurch hinter einer Tür, die den meisten verschlossen ist. Jetzt heisst sie **Einstellungen**, steht jedem offen und hat zwei beschriftete Gruppen: oben, was einem selbst gehört (Mein Konto, Geschaut), darunter — nur für Administratoren — **Verwaltung**. Die Überschrift ist hier die halbe Aussage: sie sagt ohne weiteren Satz, wem der untere Teil gehört. In der Hauptleiste steht der Eintrag jetzt unten, ohne Gruppenüberschrift. Eine Überschrift über einem einzigen Eintrag ist eine Überschrift über nichts, und am Fuss sucht man Einstellungen ohnehin. `#/profil` und `#/verwaltung/…` leiten weiter; ein Lesezeichen merkt davon nichts. **Ein kurzes Menü am Bild.** Erst meldete der Klick ab — ohne Rückfrage. Dann führte er auf die eigene Seite, und zum Abmelden musste man erst dorthin. Beides sind zwei Punkte; ein Menü mit zwei Punkten ist ehrlicher als eine Tür, hinter der eine zweite liegt. **Und das Profilbild lässt sich zuschneiden.** Ein Profilbild ist rund, was hochgeladen wird selten quadratisch — den Ausschnitt dem Browser zu überlassen hiesse: mittig, und das trifft bei einem Porträt den Hals. Der Dialog zeigt den runden Ausschnitt, man zieht und vergrössert, und hochgeladen wird genau das: ein Quadrat von 512 Pixeln. Gemessen, es kommt auch so an. **Dazu die Spielerleiste.** Beide Knopfgruppen durften schrumpfen, ihre Knöpfe aber nicht — wird es eng, steht der äusserste halb hinter dem Fensterrand. Rechts sind es nur Knöpfe, da gibt es nichts nachzugeben; links schon. Dazu seitlich `env(safe-area-inset-*)`, damit auf einem Gerät mit runden Ecken nichts darunter liegt. Gemessen: vorher riss es ab 420 Pixeln, jetzt bis 320 nicht mehr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.8 KiB
TypeScript
150 lines
5.8 KiB
TypeScript
import { statistik, bildAdresse, type Statistik } from '../../api';
|
|
import { esc, fehlerzeile, meldung } from '../../util';
|
|
|
|
/** Was man geschaut hat — die eigenen Zahlen, und nur die eigenen.
|
|
*
|
|
* **Jeder sieht sich selbst, auch ein Administrator.** Das ist die Linie, die
|
|
* im Server schon steht: wer wann welchen Film anschaut, ist eine Auskunft
|
|
* über Menschen und nicht über den Server.
|
|
*/
|
|
export function geschautseite(): HTMLElement {
|
|
const wurzel = document.createElement('div');
|
|
wurzel.className = 'profil-zahlen';
|
|
void zahlenLaden(wurzel);
|
|
return wurzel;
|
|
}
|
|
|
|
async function zahlenLaden(ziel: HTMLElement): Promise<void> {
|
|
ziel.innerHTML = '<div class="spinner"></div>';
|
|
let stand: Statistik;
|
|
try {
|
|
stand = await statistik();
|
|
} catch (fehler) {
|
|
ziel.innerHTML = '';
|
|
ziel.appendChild(fehlerzeile(meldung(fehler)));
|
|
return;
|
|
}
|
|
|
|
ziel.innerHTML = '';
|
|
const h = document.createElement('h2');
|
|
h.textContent = 'Geschaut';
|
|
ziel.appendChild(h);
|
|
|
|
// **Drei Zahlen, und jede sagt, woher sie kommt.**
|
|
//
|
|
// Die Gesamtzeit stammt aus dem Protokoll und fängt an dem Tag an, an dem
|
|
// es eingebaut wurde. Die Zahl gesehener Titel steht seit jeher fest. Das
|
|
// zu einer Zahl zu verrühren hiesse, die eine zu behaupten.
|
|
const kacheln = document.createElement('div');
|
|
kacheln.className = 'zahlenreihe';
|
|
kacheln.append(
|
|
zahl(stunden(stand.loggedSeconds), 'aufgezeichnet',
|
|
stand.since
|
|
? `seit ${new Date(stand.since).toLocaleDateString('de-CH', { dateStyle: 'medium' })}`
|
|
: 'noch nichts aufgezeichnet'),
|
|
zahl(String(stand.watchedTitles), stand.watchedTitles === 1 ? 'Titel gesehen' : 'Titel gesehen',
|
|
'mit Häkchen, auch von früher'),
|
|
zahl(stunden(stand.watchedRuntimeSeconds), 'Laufzeit davon',
|
|
'die Länge aller abgehakten Titel'),
|
|
);
|
|
ziel.appendChild(kacheln);
|
|
|
|
ziel.appendChild(wochenkurve(stand));
|
|
|
|
if (stand.genres.length > 0) ziel.appendChild(genreliste(stand));
|
|
if (stand.recent.length > 0) ziel.appendChild(zuletztliste(stand));
|
|
}
|
|
|
|
/** Die Wochenkurve als Balken — zwölf Werte brauchen keine Bibliothek. */
|
|
|
|
function wochenkurve(stand: Statistik): HTMLElement {
|
|
const block = document.createElement('div');
|
|
block.className = 'wochen';
|
|
|
|
const hoechstwert = Math.max(...stand.weeks.map(w => w.seconds), 1);
|
|
stand.weeks.forEach(w => {
|
|
const saeule = document.createElement('div');
|
|
saeule.className = 'woche';
|
|
const balken = document.createElement('div');
|
|
balken.className = 'woche-balken';
|
|
// **In Pixeln, nicht in Prozent.** Eine Prozenthöhe in einer Flexspalte
|
|
// löst sich nicht gegen das auf, was man erwartet — gemessen: aus „2 %"
|
|
// wurden 48 Pixel. Hundert Pixel sind der Platz, der da ist.
|
|
// Mindestens zwei, damit eine leere Woche als leer zu sehen ist und
|
|
// nicht als fehlend.
|
|
balken.style.height = `${Math.max(2, Math.round((w.seconds / hoechstwert) * 100))}px`;
|
|
// **`woche-leer`, nicht `leer`.** Den kurzen Namen gibt es schon: er
|
|
// gehört der Fläche „hier ist nichts" und bringt 24 Pixel Polster mit.
|
|
// Der Balken wurde dadurch 48 Pixel hoch, egal welche Höhe dastand.
|
|
if (w.seconds === 0) balken.classList.add('woche-leer');
|
|
const datum = new Date(w.start);
|
|
saeule.title = `Woche ab ${datum.toLocaleDateString('de-CH', { dateStyle: 'medium' })}: `
|
|
+ (w.seconds > 0 ? stunden(w.seconds) : 'nichts');
|
|
const marke = document.createElement('span');
|
|
marke.className = 'woche-marke';
|
|
marke.textContent = datum.toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit' });
|
|
saeule.append(balken, marke);
|
|
block.appendChild(saeule);
|
|
});
|
|
|
|
return block;
|
|
}
|
|
|
|
function genreliste(stand: Statistik): HTMLElement {
|
|
const block = document.createElement('div');
|
|
block.className = 'genres';
|
|
const groesste = Math.max(...stand.genres.map(g => g.seconds), 1);
|
|
stand.genres.forEach(g => {
|
|
const zeileG = document.createElement('div');
|
|
zeileG.className = 'genre';
|
|
zeileG.innerHTML = `<span class="genre-name">${esc(g.name)}</span>`
|
|
+ `<span class="genre-balken"><i style="width:${Math.round((g.seconds / groesste) * 100)}%"></i></span>`
|
|
+ `<span class="genre-wert">${esc(stunden(g.seconds))}</span>`;
|
|
block.appendChild(zeileG);
|
|
});
|
|
return block;
|
|
}
|
|
|
|
function zuletztliste(stand: Statistik): HTMLElement {
|
|
const block = document.createElement('div');
|
|
block.className = 'zuletzt';
|
|
|
|
const h = document.createElement('h3');
|
|
h.textContent = 'Zuletzt gesehen';
|
|
block.appendChild(h);
|
|
|
|
stand.recent.slice(0, 8).forEach(e => {
|
|
const k = document.createElement('button');
|
|
k.type = 'button';
|
|
k.className = 'zuletzt-zeile';
|
|
const adresse = bildAdresse(e.posterImage);
|
|
k.innerHTML = (adresse ? `<img src="${adresse}" alt="">` : '<span class="platzhalter"></span>')
|
|
+ `<span class="zuletzt-text"><strong>${esc(e.title)}</strong>`
|
|
+ (e.subTitle ? `<span class="winzig">${esc(e.subTitle)}</span>` : '')
|
|
+ `</span>`
|
|
+ `<span class="winzig">${esc(new Date(e.at).toLocaleDateString('de-CH', { dateStyle: 'medium' }))}</span>`;
|
|
k.onclick = () => { location.hash = `#/${e.kind === 'episode' ? 'serien' : 'filme'}/${e.id}`; };
|
|
block.appendChild(k);
|
|
});
|
|
|
|
return block;
|
|
}
|
|
|
|
function zahl(wert: string, was: string, hinweis: string): HTMLElement {
|
|
const d = document.createElement('div');
|
|
d.className = 'zahlkachel';
|
|
d.innerHTML = `<strong>${esc(wert)}</strong><span>${esc(was)}</span>`
|
|
+ `<span class="winzig">${esc(hinweis)}</span>`;
|
|
return d;
|
|
}
|
|
|
|
/* ---------- Bild und Name ---------- */
|
|
|
|
/** „12 Std. 30 Min." — oder „0 Min.", solange nichts dasteht. */
|
|
function stunden(sekunden: number): string {
|
|
if (sekunden < 60) return '0 Min.';
|
|
const h = Math.floor(sekunden / 3600);
|
|
const m = Math.round((sekunden % 3600) / 60);
|
|
return h > 0 ? `${h} Std. ${m} Min.` : `${m} Min.`;
|
|
}
|