fix(shorts): hover-chrome, swipe mobile garanti, avance auto fiabilisee
CI / build-and-test (push) Successful in 15m2s
CI / build-and-test (push) Successful in 15m2s
Chrome auto-hide au hover desktop (2,5 s). Capteur tactile mobile : swipe garanti (l'iframe mangeait les touchers), tap = play/pause YT ou 6 s d'acces direct au lecteur sinon. Fleche retour accueil mobile, cadre quasi plein ecran. Avance auto : handshake YT listening (les evenements player n'arrivaient jamais), onStateChange canonique, retry borne anti-prefetch. Docs 9.9.
This commit is contained in:
@@ -486,6 +486,8 @@ La table est **additive**. Le Phase 5.3 (LEFT JOIN) est le seul changement de le
|
||||
- [x] Son et lecture par provider : son par défaut (YT/OD/DM/PT), clips Twitch en muet forcé (pas d'API de volume sur l'embed) ; Odysee démarre muet (pas de rectangle « tap to unmute ») et ne passe au son que sur clic explicite du bouton Son (le geste autorise l'autoplay sonore) ; nudge `playVideo` YouTube si l'autoplay a calé.
|
||||
- [x] Avance auto en fin de vidéo tous providers : `playerState` YouTube (0 = fin → suivant, 2 = pause → minuteur désarmé, 1 = reprise → réarmé) + minuteur générique sur durée connue ou estimée (clip Twitch = 60 s, sinon plafond Short 90 s) + 2,5 s de grâce, ignoré si l'onglet est caché.
|
||||
- [x] Anti-Forbidden Dailymotion : `allow_embed` demandé côté serveur → `embeddable` → `raw.allowEmbed`, le garde `isPlayable` existant exclut ces vidéos avant affichage (même garde que le chemin historique).
|
||||
- [x] Chrome auto-hide au hover (desktop, 2,5 s d'immobilité) ; capteur tactile mobile (l'iframe mange les touchers) : swipe = navigation garantie, tap = play/pause YouTube (API iframe) ou 6 s d'accès direct au lecteur sinon (les boutons du fournisseur restent atteignables) ; flèche retour accueil en haut à gauche sur mobile ; cadre quasi plein écran (`--shorts-chrome: 5,5rem`).
|
||||
- [x] Avance auto fiabilisée : handshake `listening` YouTube SANS LEQUEL aucun événement player n'arrive (progression + fin mortes), `onStateChange` canonique, réessais bornés quand `next()` est absorbé par un pré-chargement en cours.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -507,9 +507,10 @@ body {
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
/* Mobile : cadre quasi plein écran (seuls les chips + marges sont réservés). */
|
||||
/* Mobile : quasi plein écran (seule la rangée de chips est réservée :
|
||||
la flèche retour et le chrome vivent EN OVERLAY dans le cadre). */
|
||||
.shorts-frame {
|
||||
--shorts-chrome: 8.5rem;
|
||||
--shorts-chrome: 5.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,6 +548,30 @@ body {
|
||||
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* --- Shorts : chrome auto-hide + capteur tactile ---------------------------- */
|
||||
/* Desktop : le chrome (pastilles, boutons, rail, méta) s'efface après 2,5 s
|
||||
d'immobilité de la souris et revient au moindre mouvement (façon YouTube).
|
||||
Sur tactile, `mousemove` ne tire presque jamais : tout reste visible. */
|
||||
.shorts-chrome {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.shorts-chrome-hidden .shorts-chrome {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.shorts-chrome {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
/* Capteur tactile mobile : couche transparente AU-DESSUS de l'iframe (qui
|
||||
mangerait les touchers) mais SOUS le chrome. `touch-action: none` donne le
|
||||
geste à nos handlers (swipe = navigation, tap = play/pause ou accès
|
||||
lecteur) au lieu de scroller/zoomer la page. */
|
||||
.shorts-gesture {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* --- Drawer / overlay plumbing -------------------------------------------- */
|
||||
/* Applied to <body> while a full-screen drawer or modal owns the viewport.
|
||||
Note: deliberately NOT `touch-action: none` — touch-action is intersected up
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="shorts-stage relative flex min-h-[calc(var(--app-vh,100dvh)-var(--header-offset,0px))] w-full flex-col items-center overflow-hidden bg-black" (click)="closeMenu()">
|
||||
<div class="shorts-stage relative flex min-h-[calc(var(--app-vh,100dvh)-var(--header-offset,0px))] w-full flex-col items-center overflow-hidden bg-black"
|
||||
[class.shorts-chrome-hidden]="!chromeVisible()"
|
||||
(click)="closeMenu()" (mousemove)="pokeChrome()">
|
||||
<!-- Fond d'ambiance : vignette du short courant, floutée (façon YouTube). -->
|
||||
<div class="shorts-ambient" aria-hidden="true">
|
||||
@if (current()?.thumbnail) {
|
||||
@@ -6,8 +8,15 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Retour accueil (mobile plein écran : la vidéo occupe tout, pas de header visible) -->
|
||||
<a routerLink="/"
|
||||
class="absolute top-2 left-2 z-20 flex h-11 w-11 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80 sm:hidden"
|
||||
title="Retour à l'accueil NewTube" aria-label="Retour à l'accueil NewTube" (click)="$event.stopPropagation()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5m7-7l-7 7 7 7"/></svg>
|
||||
</a>
|
||||
|
||||
<!-- Sélecteur : Pour toi + fournisseurs -->
|
||||
<div class="scroll-rail no-scrollbar relative z-10 flex w-full max-w-full items-center justify-start gap-2 overflow-x-auto px-3 pt-3 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts" (click)="$event.stopPropagation()">
|
||||
<div class="scroll-rail no-scrollbar shorts-chrome relative z-10 flex w-full max-w-full items-center justify-start gap-2 overflow-x-auto px-3 pt-3 sm:justify-center" role="tablist" aria-label="Fournisseur des Shorts" (click)="$event.stopPropagation()">
|
||||
<button (click)="toggleForYou()"
|
||||
[attr.aria-pressed]="forYou()"
|
||||
class="chip shrink-0 px-3 py-2 rounded-full text-[13px] border transition-colors min-h-[44px]"
|
||||
@@ -29,7 +38,7 @@
|
||||
(wheel)="onWheel($event)" (touchstart)="onTouchStart($event)" (touchend)="onTouchEnd($event)">
|
||||
<!-- Flèche précédent (bureau, façon YouTube) -->
|
||||
<button type="button" (click)="prev(); $event.stopPropagation()" [disabled]="!canPrev()"
|
||||
class="chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30"
|
||||
class="shorts-chrome chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30"
|
||||
aria-label="Short précédent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 14l5-5 5 5H7z"/></svg>
|
||||
</button>
|
||||
@@ -55,8 +64,23 @@
|
||||
}
|
||||
|
||||
@if (current(); as v) {
|
||||
<!-- Capteur tactile (mobile uniquement) : l'iframe mange les
|
||||
touchers, donc le swipe up/down passerait à la trappe. Cette
|
||||
couche transparente capte swipe (navigue) et tap (play/pause
|
||||
YouTube, ou accès direct au lecteur 6 s sinon). `touch-action`
|
||||
via CSS : le navigateur ne scrolle/zoome pas, nos handlers
|
||||
possèdent le geste. En mode passthrough, elle s'efface
|
||||
(les boutons du fournisseur deviennent touchables). -->
|
||||
<div class="shorts-gesture absolute inset-0 z-[1] sm:hidden"
|
||||
[class.pointer-events-none]="passthrough()"
|
||||
aria-hidden="true"></div>
|
||||
|
||||
@if (passthroughHint()) {
|
||||
<div class="absolute left-1/2 top-14 z-20 -translate-x-1/2 whitespace-nowrap px-3 py-1.5 rounded-full bg-black/75 text-white text-xs" role="status">Commandes du lecteur accessibles</div>
|
||||
}
|
||||
|
||||
<!-- Haut droite : plein écran, partage rapide, menu ⋯ -->
|
||||
<div class="absolute top-2 right-2 flex items-center gap-1.5" (click)="$event.stopPropagation()">
|
||||
<div class="shorts-chrome absolute top-2 right-2 z-10 flex items-center gap-1.5" (click)="$event.stopPropagation()">
|
||||
<button type="button" (click)="toggleFullscreen()" class="flex h-10 w-10 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80" [title]="isFullscreen() ? 'Quitter le plein écran' : 'Plein écran'" aria-label="Plein écran">
|
||||
@if (isFullscreen()) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>
|
||||
@@ -86,7 +110,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Rail d'actions OVERLAY façon YouTube (dans le cadre, comme sur mobile natif) -->
|
||||
<div class="absolute right-1.5 sm:right-2.5 bottom-32 sm:bottom-36 flex flex-col items-center gap-2 sm:gap-2.5" (click)="$event.stopPropagation()">
|
||||
<div class="shorts-chrome absolute right-1.5 sm:right-2.5 bottom-32 sm:bottom-36 z-10 flex flex-col items-center gap-2 sm:gap-2.5" (click)="$event.stopPropagation()">
|
||||
<div class="flex flex-col items-center">
|
||||
<button type="button" (click)="toggleLike()" [disabled]="!isLoggedIn() || likeBusy()"
|
||||
class="flex h-12 w-12 items-center justify-center rounded-full bg-black/55 text-white hover:bg-black/75 disabled:opacity-40"
|
||||
@@ -126,7 +150,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Bas : avatar + titre + méta + fraîcheur + S'abonner (safe-area incluse) -->
|
||||
<div class="shorts-meta pointer-events-none absolute left-0 right-0 bottom-0 p-3 sm:p-4 text-white bg-gradient-to-t from-black/85 via-black/35 to-transparent">
|
||||
<div class="shorts-meta shorts-chrome pointer-events-none absolute left-0 right-0 bottom-0 z-10 p-3 sm:p-4 text-white bg-gradient-to-t from-black/85 via-black/35 to-transparent">
|
||||
<div class="flex items-end gap-2.5 text-sm" (click)="$event.stopPropagation()">
|
||||
@if (v.uploaderAvatar) {
|
||||
<img [src]="v.uploaderAvatar" [alt]="v.uploaderName" class="h-10 w-10 shrink-0 rounded-full object-cover ring-2 ring-white/40" loading="lazy" />
|
||||
@@ -180,7 +204,7 @@
|
||||
|
||||
<!-- Flèche suivant (bureau, façon YouTube) -->
|
||||
<button type="button" (click)="next(); $event.stopPropagation()" [disabled]="!canNext()"
|
||||
class="chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30"
|
||||
class="shorts-chrome chip hidden sm:flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:opacity-30"
|
||||
aria-label="Short suivant">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5H7z"/></svg>
|
||||
</button>
|
||||
|
||||
@@ -518,6 +518,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const i = this.index();
|
||||
const list = this.items();
|
||||
this.ytProgress.set(null);
|
||||
this.ytPlaying.set(true);
|
||||
this.menuOpen.set(false);
|
||||
if (!v) return;
|
||||
const nx = (i >= 0 && i + 1 < list.length) ? list[i + 1] : null;
|
||||
@@ -559,6 +560,10 @@ export class WatchShortComponent implements OnDestroy {
|
||||
try { document.removeEventListener('fullscreenchange', this.handleFullscreenChange); } catch {}
|
||||
try { if (this.autoAdvanceTimer) clearTimeout(this.autoAdvanceTimer); } catch {}
|
||||
this.autoAdvanceTimer = null;
|
||||
try { if (this.chromeIdleTimer) clearTimeout(this.chromeIdleTimer); } catch {}
|
||||
this.chromeIdleTimer = null;
|
||||
try { if (this.passthroughTimer) clearTimeout(this.passthroughTimer); } catch {}
|
||||
this.passthroughTimer = null;
|
||||
if (this.shareCopiedTimer) { try { clearTimeout(this.shareCopiedTimer); } catch {} }
|
||||
}
|
||||
|
||||
@@ -600,6 +605,15 @@ export class WatchShortComponent implements OnDestroy {
|
||||
/** Timer d'avance auto en fin de vidéo (repli générique). */
|
||||
private autoAdvanceTimer: any = null;
|
||||
private autoAdvanceKey = '';
|
||||
/** État de lecture YouTube (via `playerState`, pour le toggle tap). */
|
||||
private ytPlaying = signal(true);
|
||||
/** Chrome (boutons/méta) visible — auto-hide sur desktop au repos. */
|
||||
readonly chromeVisible = signal(true);
|
||||
private chromeIdleTimer: any = null;
|
||||
/** Accès direct au lecteur (overlay effacé 6 s, tap non-YouTube). */
|
||||
readonly passthrough = signal(false);
|
||||
readonly passthroughHint = signal(false);
|
||||
private passthroughTimer: any = null;
|
||||
|
||||
/**
|
||||
* Durée effective pour l'avance auto quand la durée réelle est inconnue.
|
||||
@@ -649,11 +663,43 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (typeof document !== 'undefined' && document.hidden) return;
|
||||
if (this.autoAdvanceKey !== key) return;
|
||||
const cur = this.current();
|
||||
if (this.index() === atIndex && cur && String(cur.videoId) === id) this.next();
|
||||
if (this.index() === atIndex && cur && String(cur.videoId) === id) this.advanceWithRetry();
|
||||
} catch {}
|
||||
}, Math.min(600000, (secs + 2.5) * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Avance avec réessais bornés : `next()` peut être absorbé sans effet quand
|
||||
* un pré-chargement est en cours (`busyMore`) ou que la page suivante n'est
|
||||
* pas encore arrivée — sans réessai, la vidéo reste bloquée sur son écran
|
||||
* de fin. 2 réessais à 2,5 s puis abandon (jamais de boucle).
|
||||
*/
|
||||
private advanceWithRetry(attempt = 0): void {
|
||||
try {
|
||||
const before = this.index();
|
||||
if (!this.items().length || this.loading()) return;
|
||||
this.next();
|
||||
if (this.index() !== before) return;
|
||||
if (attempt < 2) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (this.index() === before && this.items().length > 0 && !this.loading()) {
|
||||
this.advanceWithRetry(attempt + 1);
|
||||
}
|
||||
} catch {}
|
||||
}, 2500);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** Fin de lecture vue par un player (garde anti-double). */
|
||||
private advanceFromPlayer(id: string): void {
|
||||
const key = `end:${id}`;
|
||||
if (this.autoAdvanceKey === key) return;
|
||||
this.autoAdvanceKey = key;
|
||||
this.advanceWithRetry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relance la lecture YouTube si l'autoplay a calé : commande `playVideo`
|
||||
* via l'API iframe (`enablejsapi=1` déjà présent dans l'URL d'embed).
|
||||
@@ -664,18 +710,30 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (!v?.videoId || this.providerOf(v) !== 'youtube') return;
|
||||
const id = String(v.videoId);
|
||||
const atIndex = this.index();
|
||||
// Handshake SANS LEQUEL le player YouTube n'émet RIEN (ni infoDelivery
|
||||
// ni onStateChange) : c'est ce qui rendait l'avance auto et la barre de
|
||||
// progression inopérantes. Envoyé tout de suite + après chargement.
|
||||
this.postToPlayer({ event: 'listening' });
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const cur = this.current();
|
||||
if (this.index() !== atIndex || !cur || String(cur.videoId) !== id) return;
|
||||
const frame = this.frameRef()?.nativeElement;
|
||||
const iframe = frame?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||
iframe?.contentWindow?.postMessage(JSON.stringify({ event: 'command', func: 'playVideo', args: '' }), '*');
|
||||
this.postToPlayer({ event: 'listening' });
|
||||
if (this.ytPlaying()) this.postToPlayer({ event: 'command', func: 'playVideo', args: '' });
|
||||
} catch {}
|
||||
}, 1500);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** Post un message à l'iframe du short courant (API YouTube, sans effet sinon). */
|
||||
private postToPlayer(msg: object): void {
|
||||
try {
|
||||
const frame = this.frameRef()?.nativeElement;
|
||||
const iframe = frame?.querySelector('iframe') as HTMLIFrameElement | null;
|
||||
iframe?.contentWindow?.postMessage(JSON.stringify(msg), '*');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* File de résolution Rumble (concurrence bornée) : chaque id est vérifié
|
||||
* via le backend (comme /watch). Succès => l'embed bascule sur le vrai
|
||||
@@ -743,6 +801,55 @@ export class WatchShortComponent implements OnDestroy {
|
||||
this.muted.update(m => !m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome auto-hide (desktop) : visible au survol / à l'interaction, masqué
|
||||
* après 2,5 s d'immobilité — façon YouTube. Sur tactile, `mousemove` ne
|
||||
* tire presque jamais : le chrome reste visible en permanence.
|
||||
*/
|
||||
pokeChrome(): void {
|
||||
try {
|
||||
this.chromeVisible.set(true);
|
||||
if (this.chromeIdleTimer) clearTimeout(this.chromeIdleTimer);
|
||||
this.chromeIdleTimer = setTimeout(() => {
|
||||
try { this.chromeVisible.set(false); } catch {}
|
||||
}, 2500);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tap sur la vidéo (mobile : l'overlay capte le toucher, l'iframe ne le
|
||||
* voit pas — voir `.shorts-gesture`) :
|
||||
* - YouTube : play/pause via l'API iframe (état suivi par `playerState`) ;
|
||||
* - autres (pas d'API) : 6 s d'accès DIRECT au lecteur — l'overlay
|
||||
* s'efface et les boutons du fournisseur deviennent touchables, puis
|
||||
* l'overlay revient seul. Discret et réversible.
|
||||
*/
|
||||
private onVideoTap(): void {
|
||||
try {
|
||||
const v = this.current();
|
||||
if (!v) return;
|
||||
this.pokeChrome();
|
||||
if (this.providerOf(v) === 'youtube') {
|
||||
const play = !this.ytPlaying();
|
||||
this.postToPlayer({ event: 'command', func: play ? 'playVideo' : 'pauseVideo', args: '' });
|
||||
this.ytPlaying.set(play);
|
||||
return;
|
||||
}
|
||||
this.enterPassthrough();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private enterPassthrough(): void {
|
||||
try {
|
||||
this.passthrough.set(true);
|
||||
this.passthroughHint.set(true);
|
||||
if (this.passthroughTimer) clearTimeout(this.passthroughTimer);
|
||||
this.passthroughTimer = setTimeout(() => {
|
||||
try { this.passthrough.set(false); this.passthroughHint.set(false); } catch {}
|
||||
}, 6000);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
toggleFullscreen(): void {
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
@@ -857,6 +964,26 @@ export class WatchShortComponent implements OnDestroy {
|
||||
try { this.skipUnplayable(); } catch {}
|
||||
return;
|
||||
}
|
||||
// Fin/début/pause canoniques (`onStateChange`, plus fiable que le temps :
|
||||
// pas de marge). Requiert le handshake `listening` (voir nudgeYouTubePlay).
|
||||
if (data.event === 'onStateChange') {
|
||||
const st = Number(data.info);
|
||||
const cur = this.current();
|
||||
if (!cur || String(cur.videoId) !== String(v.videoId) || !Number.isFinite(st)) return;
|
||||
const id = String(v.videoId);
|
||||
if (st === 0) {
|
||||
this.advanceFromPlayer(id);
|
||||
} else if (st === 2) {
|
||||
// Pause utilisateur : état + minuteur désarmé (on n'avance pas une
|
||||
// vidéo mise en pause).
|
||||
this.ytPlaying.set(false);
|
||||
this.autoAdvanceKey = `paused:${id}`;
|
||||
} else if (st === 1) {
|
||||
this.ytPlaying.set(true);
|
||||
if (this.autoAdvanceKey.startsWith('paused:')) this.scheduleAutoAdvance(v);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.event === 'infoDelivery' && data.info && typeof data.info === 'object') {
|
||||
const ct = Number(data.info.currentTime);
|
||||
let dur = Number(data.info.duration);
|
||||
@@ -869,23 +996,21 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const id = String(v.videoId);
|
||||
if (state === 0) {
|
||||
// Fin certaine (plus fiable que le temps : pas de marge) → suivant.
|
||||
const key = `end:${id}`;
|
||||
if (this.autoAdvanceKey !== key) {
|
||||
this.autoAdvanceKey = key;
|
||||
this.next();
|
||||
}
|
||||
this.advanceFromPlayer(id);
|
||||
return;
|
||||
}
|
||||
if (state === 2) {
|
||||
// Pause utilisateur : on désarme le minuteur générique pour ne
|
||||
// pas faire avancer une vidéo mise en pause.
|
||||
this.ytPlaying.set(false);
|
||||
this.autoAdvanceKey = `paused:${id}`;
|
||||
return;
|
||||
}
|
||||
if (state === 1 && this.autoAdvanceKey.startsWith('paused:')) {
|
||||
if (state === 1) {
|
||||
// Reprise après pause : on réarme le minuteur (repli, la fin
|
||||
// temps réel prime de toute façon).
|
||||
this.scheduleAutoAdvance(v);
|
||||
this.ytPlaying.set(true);
|
||||
if (this.autoAdvanceKey.startsWith('paused:')) this.scheduleAutoAdvance(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -898,11 +1023,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
if (ct >= dur - 0.75) {
|
||||
const cur = this.current();
|
||||
if (cur && String(cur.videoId) === String(v.videoId)) {
|
||||
const key = `end:${String(v.videoId)}`;
|
||||
if (this.autoAdvanceKey !== key) {
|
||||
this.autoAdvanceKey = key;
|
||||
this.next();
|
||||
}
|
||||
this.advanceFromPlayer(String(v.videoId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1730,6 +1851,7 @@ export class WatchShortComponent implements OnDestroy {
|
||||
const threshold = 50;
|
||||
if (dy < -threshold) this.next();
|
||||
else if (dy > threshold) this.prev();
|
||||
else if (Math.abs(dy) < 12) this.onVideoTap();
|
||||
this.lastSwipeY = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user