Nouveau formulaire "Recherche YouTube" (mots-cles + limite optionnelle), branche sur la zone de resultats existante (miniatures, bouton "Ajouter" par ligne ; pas de "Tout ajouter", comme demande). - Avec une limite : tous les resultats d'un coup (une seule page). - Sans limite : defilement infini. Un element sentinelle en fin de liste, observe par un IntersectionObserver (racine = la liste, marge 300 px), charge la page suivante ; il est rearme apres chaque page, affiche chargement / erreur avec "Reessayer" / "Fin des resultats", et une page en echec ne fait pas perdre les precedentes. Serveur (GET/POST/DELETE /api/search) : une "session" = un seul yt-dlp (--flat-playlist --lazy-playlist -j -- ytsearch1000:<requete>) servi page par page depuis un tampon. Pourquoi pas un appel yt-dlp par page - mesure avec le vrai yt-dlp : l'ordre des resultats de YouTube n'est pas deterministe meme pour deux appels identiques (5/20 aux memes positions, 69 % de recouvrement sur 100), donc paginer en relancant la recherche donnait 15 a 30 % de doublons des la 3e-4e page et des trous ; YouTube s'arrete vers ~590 resultats ; ~7 % de doublons internes (dedoublonnes a l'ingestion) ; sans --lazy-playlist yt-dlp n'imprime rien avant la fin (~23 s), avec il emet au fil des pages (20 en ~2,3 s). Le parcours n'avance qu'a la demande : quand le tampon a 20 resultats d'avance, la lecture de stdout est suspendue (nouvelle option onSpawn de runYtDlp) ; le tube se remplit et yt-dlp bloque (mesure : plus aucune ligne pendant 10 s de pause). On n'aspire donc pas ~590 resultats, et ~30 requetes vers YouTube, quand l'utilisateur ne defile pas. Cycle de vie : 4 sessions simultanees max (la moins recemment utilisee est fermee), expiration a 10 min d'inactivite / 30 min de vie, fermeture des que la derniere page est servie, sur DELETE (liste remplacee cote client), au pagehide du client, et a l'arret du serveur. Requete validee (1 a 200 caracteres, sans caractere de controle) et transmise en un seul argument ytsearchN:<requete>, jamais interpretable comme une option. Mise en page : ce troisieme formulaire laissait a la liste ~104 px (une ligne) a 1366x650. Une liste non vide garde min-height 13rem et la colonne du formulaire defile au lieu de rogner (la page ne defile toujours pas). Verifie : contre le vrai yt-dlp (page tamponnee en 4 ms, page 200 apres 12 s d'inactivite en ~4 s : la suspension tient ; 29 pages -> 576 videos, 576 uniques ; processus fermes a la fin / sur DELETE / au plafond) et dans le navigateur (580 resultats sans doublon jusqu'a "Fin des resultats", requetes de page uniques malgre deux signaux simultanes, echec + reprise, session expiree, remplacement de la recherche, mode limite, ajout d'un resultat a la file, pagehide, playlist et chaine en non-regression). Le volet IntersectionObserver a ete pilote par un observateur factice : le panneau de navigation n'affichait plus de frames (rAF/IO natifs muets). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
122 lines
2.8 KiB
SCSS
122 lines
2.8 KiB
SCSS
// Palette et styles de base partagés par toutes les vues. Variables CSS
|
|
// natives (pas des $variables Sass) : chaque vue compile son propre
|
|
// fichier .scss indépendamment (voir build.mjs), seules les custom
|
|
// properties CSS traversent correctement ce découpage à l'exécution.
|
|
:root {
|
|
--color-bg: #f6f7f9;
|
|
--color-surface: #ffffff;
|
|
--color-border: #e3e5ea;
|
|
--color-text: #2b2d33;
|
|
--color-text-muted: #767a85;
|
|
--color-primary: #4f7cff;
|
|
--color-primary-hover: #3d64e0;
|
|
--color-danger-bg: #fdf2f2;
|
|
--color-danger-text: #b3423f;
|
|
--color-danger-border: #eab8b6;
|
|
--color-success-border: #5fae84;
|
|
--color-neutral-border: #c7cad1;
|
|
--radius: 8px;
|
|
}
|
|
|
|
// Thème sombre selon la préférence système (`prefers-color-scheme`), pas
|
|
// de bascule manuelle demandée : mêmes custom properties, valeurs
|
|
// réadaptées pour le contraste (tout le reste du CSS n'y touche jamais
|
|
// directement, voir plus bas - seule cette feuille définit des couleurs).
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--color-bg: #16181d;
|
|
--color-surface: #1f2229;
|
|
--color-border: #33363f;
|
|
--color-text: #e7e8ea;
|
|
--color-text-muted: #9498a3;
|
|
--color-primary: #5b84ff;
|
|
--color-primary-hover: #7899ff;
|
|
--color-danger-bg: #3a2222;
|
|
--color-danger-text: #e59a97;
|
|
--color-danger-border: #6b3937;
|
|
--color-success-border: #4f9c72;
|
|
--color-neutral-border: #494d59;
|
|
}
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
// La page elle-même ne défile jamais : seules les listes internes (file
|
|
// d'attente, résultats de recherche) le font, voir Shell.scss et les
|
|
// vues concernées (chacune doit propager height:100%/min-height:0
|
|
// jusqu'à son propre élément scrollable pour que ça fonctionne).
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
font-family:
|
|
system-ui,
|
|
-apple-system,
|
|
'Segoe UI',
|
|
sans-serif;
|
|
}
|
|
|
|
input,
|
|
select,
|
|
button {
|
|
font: inherit;
|
|
color: inherit;
|
|
}
|
|
|
|
input[type='text'],
|
|
input[type='url'],
|
|
input[type='number'],
|
|
select {
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius);
|
|
padding: 0.5rem 0.6rem;
|
|
background: var(--color-surface);
|
|
}
|
|
|
|
input:focus,
|
|
select:focus,
|
|
button:focus-visible {
|
|
outline: 2px solid var(--color-primary);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
button {
|
|
border: 1px solid var(--color-primary);
|
|
background: var(--color-primary);
|
|
color: white;
|
|
border-radius: var(--radius);
|
|
padding: 0.5rem 0.9rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
button:hover {
|
|
background: var(--color-primary-hover);
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.55;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
button.secondary {
|
|
background: var(--color-surface);
|
|
color: var(--color-text);
|
|
border-color: var(--color-border);
|
|
}
|
|
|
|
button.secondary:hover {
|
|
background: var(--color-bg);
|
|
}
|
|
|
|
a {
|
|
color: var(--color-primary);
|
|
}
|