tiktok feed mode
This commit is contained in:
@@ -1359,3 +1359,211 @@ body.theme-light .error-toast {
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Mode toggle FAB (grid <-> reels) */
|
||||
.mode-toggle-btn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 90px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-secondary);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px var(--shadow);
|
||||
z-index: 2600;
|
||||
transition: transform 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-toggle-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.mode-toggle-btn:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
body.feed-mode-open .mode-toggle-btn {
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
body.feed-mode-open .mode-toggle-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
body.feed-mode-open .mode-toggle-btn .icon-svg {
|
||||
filter: invert(100%) saturate(0%) !important;
|
||||
}
|
||||
|
||||
/* Reels-style scrollable feed */
|
||||
.feed-view {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
z-index: 2500;
|
||||
}
|
||||
|
||||
.feed-view.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feed-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
scroll-snap-type: y mandatory;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.feed-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.feed-sentinel {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.feed-slide {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.feed-poster,
|
||||
.feed-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.feed-poster {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.feed-slide.is-loaded .feed-poster {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.feed-info {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 24px 20px 44px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.feed-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.feed-uploader {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feed-timeline {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 4px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.feed-timeline-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 999px;
|
||||
overflow: visible;
|
||||
transition: height 0.15s ease;
|
||||
}
|
||||
|
||||
.feed-timeline:hover .feed-timeline-track,
|
||||
.feed-timeline.is-scrubbing .feed-timeline-track {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.feed-timeline-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0%;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.feed-timeline-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0%;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-left: -5.5px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%) scale(0);
|
||||
transition: transform 0.15s ease;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.feed-timeline:hover .feed-timeline-handle,
|
||||
.feed-timeline.is-scrubbing .feed-timeline-handle {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
.feed-mute-btn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 154px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 2600;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.feed-mute-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.feed-mute-btn .icon-svg {
|
||||
filter: invert(100%) saturate(0%) !important;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="mode-toggle-btn" class="mode-toggle-btn" type="button" title="Switch to Reels view" aria-pressed="false">
|
||||
<img class="icon-svg" id="mode-toggle-icon" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/device-phone-mobile.svg" alt="Switch to Reels view">
|
||||
</button>
|
||||
|
||||
<div id="feed-view" class="feed-view" aria-hidden="true">
|
||||
<button id="feed-mute-btn" class="feed-mute-btn" type="button" title="Toggle mute">
|
||||
<img class="icon-svg" id="feed-mute-icon" src="https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/speaker-x-mark.svg" alt="Unmute">
|
||||
</button>
|
||||
<div id="feed-scroll" class="feed-scroll">
|
||||
<div id="feed-sentinel" class="feed-sentinel"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="info-modal" class="info-modal" aria-hidden="true">
|
||||
<div class="info-card" role="dialog" aria-modal="true" aria-labelledby="info-title">
|
||||
<button id="info-close" class="info-close" type="button" aria-label="Close">✕</button>
|
||||
@@ -145,6 +158,7 @@
|
||||
<script src="static/js/player.js"></script>
|
||||
<script src="static/js/favorites.js"></script>
|
||||
<script src="static/js/videos.js"></script>
|
||||
<script src="static/js/feed.js"></script>
|
||||
<script src="static/js/ui.js"></script>
|
||||
<script src="static/js/main.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
|
||||
312
frontend/js/feed.js
Normal file
312
frontend/js/feed.js
Normal file
@@ -0,0 +1,312 @@
|
||||
window.App = window.App || {};
|
||||
App.feed = App.feed || {};
|
||||
|
||||
(function() {
|
||||
const state = App.state;
|
||||
let observer = null;
|
||||
let sentinelObserver = null;
|
||||
|
||||
const getScroller = () => document.getElementById('feed-scroll');
|
||||
|
||||
const destroySlidePlayback = function(slide) {
|
||||
const video = slide.querySelector('.feed-video');
|
||||
slide.classList.remove('is-active');
|
||||
const fill = slide.querySelector('.feed-timeline-fill');
|
||||
if (fill) fill.style.width = '0%';
|
||||
const handle = slide.querySelector('.feed-timeline-handle');
|
||||
if (handle) handle.style.left = '0%';
|
||||
if (!video) return;
|
||||
if (video._hlsPlayer) {
|
||||
video._hlsPlayer.destroy();
|
||||
video._hlsPlayer = null;
|
||||
}
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
slide.classList.remove('is-loaded');
|
||||
};
|
||||
|
||||
const setTimelinePosition = function(slide, ratio) {
|
||||
const fill = slide.querySelector('.feed-timeline-fill');
|
||||
const handle = slide.querySelector('.feed-timeline-handle');
|
||||
const pct = `${Math.min(1, Math.max(0, ratio)) * 100}%`;
|
||||
if (fill) fill.style.width = pct;
|
||||
if (handle) handle.style.left = pct;
|
||||
};
|
||||
|
||||
const seekFromPointer = function(slide, video, timeline, clientX) {
|
||||
if (!isFinite(video.duration) || video.duration <= 0) return;
|
||||
const rect = timeline.getBoundingClientRect();
|
||||
const ratio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;
|
||||
const clamped = Math.min(1, Math.max(0, ratio));
|
||||
video.currentTime = clamped * video.duration;
|
||||
setTimelinePosition(slide, clamped);
|
||||
};
|
||||
|
||||
const bindTimeline = function(slide, video) {
|
||||
const timeline = slide.querySelector('.feed-timeline');
|
||||
if (!timeline) return;
|
||||
let scrubbing = false;
|
||||
|
||||
video.addEventListener('timeupdate', () => {
|
||||
if (scrubbing || !isFinite(video.duration) || video.duration <= 0) return;
|
||||
setTimelinePosition(slide, video.currentTime / video.duration);
|
||||
});
|
||||
|
||||
timeline.addEventListener('pointerdown', (event) => {
|
||||
scrubbing = true;
|
||||
timeline.classList.add('is-scrubbing');
|
||||
timeline.setPointerCapture(event.pointerId);
|
||||
seekFromPointer(slide, video, timeline, event.clientX);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
timeline.addEventListener('pointermove', (event) => {
|
||||
if (!scrubbing) return;
|
||||
seekFromPointer(slide, video, timeline, event.clientX);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
const stopScrubbing = (event) => {
|
||||
if (!scrubbing) return;
|
||||
scrubbing = false;
|
||||
timeline.classList.remove('is-scrubbing');
|
||||
if (timeline.hasPointerCapture(event.pointerId)) {
|
||||
timeline.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
event.stopPropagation();
|
||||
};
|
||||
timeline.addEventListener('pointerup', stopScrubbing);
|
||||
timeline.addEventListener('pointercancel', stopScrubbing);
|
||||
};
|
||||
|
||||
const PRELOAD_COUNT = 2;
|
||||
|
||||
const loadSlideSource = function(slide, videoData, autoplay) {
|
||||
const video = slide.querySelector('.feed-video');
|
||||
if (!video) return;
|
||||
if (slide.classList.contains('is-loaded')) {
|
||||
if (autoplay) {
|
||||
video.muted = state.feedMuted;
|
||||
const playPromise = video.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') playPromise.catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
slide.classList.add('is-loaded');
|
||||
|
||||
const resolved = App.videos.resolveStreamSource(videoData);
|
||||
if (!resolved.url) return;
|
||||
const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : '';
|
||||
const streamUrl = `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}`;
|
||||
const isHls = /\.m3u8($|\?)/i.test(resolved.url);
|
||||
const canUseHls = !!(window.Hls && window.Hls.isSupported());
|
||||
|
||||
video.muted = state.feedMuted;
|
||||
video.preload = 'auto';
|
||||
|
||||
if (isHls && canUseHls) {
|
||||
const hls = new window.Hls();
|
||||
video._hlsPlayer = hls;
|
||||
hls.loadSource(streamUrl);
|
||||
hls.attachMedia(video);
|
||||
hls.on(window.Hls.Events.ERROR, (event, data) => {
|
||||
if (data && data.fatal && video._hlsPlayer === hls) {
|
||||
hls.destroy();
|
||||
video._hlsPlayer = null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
video.src = streamUrl;
|
||||
}
|
||||
|
||||
if (autoplay) {
|
||||
const playPromise = video.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') playPromise.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const activateSlide = function(slide, videoData) {
|
||||
loadSlideSource(slide, videoData, true);
|
||||
};
|
||||
|
||||
// Keeps exactly [active, active+1, active+2] loaded: preloads the next
|
||||
// PRELOAD_COUNT slides and tears down everything else. Centralizing this
|
||||
// here (instead of letting each slide's own enter/leave intersection
|
||||
// entry decide) avoids a race where the initial batch of "not
|
||||
// intersecting" entries for not-yet-visible slides undoes the preload
|
||||
// we just triggered for them.
|
||||
const syncWindow = function(activeSlide) {
|
||||
const scroller = getScroller();
|
||||
if (!scroller) return;
|
||||
const slides = Array.from(scroller.querySelectorAll('.feed-slide'));
|
||||
const index = slides.indexOf(activeSlide);
|
||||
if (index === -1) return;
|
||||
|
||||
const keep = new Set();
|
||||
for (let i = index; i <= index + PRELOAD_COUNT && i < slides.length; i++) {
|
||||
keep.add(slides[i]);
|
||||
}
|
||||
slides.forEach((slide) => {
|
||||
if (keep.has(slide)) return;
|
||||
if (slide.classList.contains('is-loaded')) {
|
||||
destroySlidePlayback(slide);
|
||||
}
|
||||
});
|
||||
for (let i = index + 1; i <= index + PRELOAD_COUNT && i < slides.length; i++) {
|
||||
loadSlideSource(slides[i], slides[i]._videoData, false);
|
||||
}
|
||||
};
|
||||
|
||||
App.feed.isOpen = function() {
|
||||
return !!state.feedOpen;
|
||||
};
|
||||
|
||||
App.feed.renderSlides = function() {
|
||||
const scroller = getScroller();
|
||||
if (!scroller) return;
|
||||
(state.loadedVideos || []).forEach((v) => {
|
||||
if (state.feedRenderedIds.has(v.id)) return;
|
||||
state.feedRenderedIds.add(v.id);
|
||||
|
||||
const slide = document.createElement('div');
|
||||
slide.className = 'feed-slide';
|
||||
slide.dataset.videoId = v.id;
|
||||
const uploaderText = v.uploader || '';
|
||||
slide.innerHTML = `
|
||||
<img class="feed-poster" src="${v.thumb || ''}" alt="">
|
||||
<video class="feed-video" muted playsinline webkit-playsinline loop preload="none"></video>
|
||||
<div class="feed-info">
|
||||
<h4 class="feed-title">${v.title || ''}</h4>
|
||||
${uploaderText ? `<p class="feed-uploader">${uploaderText}</p>` : ''}
|
||||
</div>
|
||||
<div class="feed-timeline" role="slider" aria-label="Seek">
|
||||
<div class="feed-timeline-track">
|
||||
<div class="feed-timeline-fill"></div>
|
||||
<div class="feed-timeline-handle"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const poster = slide.querySelector('.feed-poster');
|
||||
App.videos.attachNoReferrerRetry(poster);
|
||||
slide._videoData = v;
|
||||
bindTimeline(slide, slide.querySelector('.feed-video'));
|
||||
scroller.insertBefore(slide, document.getElementById('feed-sentinel'));
|
||||
if (observer) observer.observe(slide);
|
||||
});
|
||||
};
|
||||
|
||||
App.feed.reset = function() {
|
||||
const scroller = getScroller();
|
||||
document.querySelectorAll('.feed-slide').forEach((slide) => {
|
||||
if (observer) observer.unobserve(slide);
|
||||
destroySlidePlayback(slide);
|
||||
slide.remove();
|
||||
});
|
||||
state.feedRenderedIds.clear();
|
||||
state.feedActiveSlide = null;
|
||||
if (scroller) scroller.scrollTop = 0;
|
||||
};
|
||||
|
||||
App.feed.open = function() {
|
||||
const container = document.getElementById('feed-view');
|
||||
const scroller = getScroller();
|
||||
if (!container || !scroller) return;
|
||||
state.feedOpen = true;
|
||||
|
||||
if (App.player && typeof App.player.close === 'function') {
|
||||
App.player.close();
|
||||
}
|
||||
|
||||
if (!observer) {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const slide = entry.target;
|
||||
if (entry.isIntersecting && entry.intersectionRatio >= 0.6 && state.feedActiveSlide !== slide) {
|
||||
const previousActive = state.feedActiveSlide;
|
||||
state.feedActiveSlide = slide;
|
||||
if (previousActive) previousActive.classList.remove('is-active');
|
||||
slide.classList.add('is-active');
|
||||
activateSlide(slide, slide._videoData);
|
||||
syncWindow(slide);
|
||||
}
|
||||
});
|
||||
}, { threshold: [0, 0.6, 1] });
|
||||
}
|
||||
|
||||
if (!sentinelObserver) {
|
||||
sentinelObserver = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
App.videos.loadVideos();
|
||||
}
|
||||
}, { threshold: 0.01 });
|
||||
const sentinel = document.getElementById('feed-sentinel');
|
||||
if (sentinel) sentinelObserver.observe(sentinel);
|
||||
}
|
||||
|
||||
App.feed.renderSlides();
|
||||
document.querySelectorAll('.feed-slide').forEach((slide) => observer.observe(slide));
|
||||
|
||||
container.classList.add('open');
|
||||
container.setAttribute('aria-hidden', 'false');
|
||||
document.body.classList.add('feed-mode-open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
App.feed.updateToggleButton();
|
||||
App.feed.updateMuteButton();
|
||||
};
|
||||
|
||||
App.feed.close = function() {
|
||||
const container = document.getElementById('feed-view');
|
||||
if (!container) return;
|
||||
state.feedOpen = false;
|
||||
document.querySelectorAll('.feed-slide').forEach((slide) => destroySlidePlayback(slide));
|
||||
state.feedActiveSlide = null;
|
||||
container.classList.remove('open');
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
document.body.classList.remove('feed-mode-open');
|
||||
document.body.style.overflow = 'auto';
|
||||
App.feed.updateToggleButton();
|
||||
};
|
||||
|
||||
App.feed.toggle = function() {
|
||||
if (state.feedOpen) {
|
||||
App.feed.close();
|
||||
} else {
|
||||
App.feed.open();
|
||||
}
|
||||
};
|
||||
|
||||
App.feed.toggleMute = function() {
|
||||
state.feedMuted = !state.feedMuted;
|
||||
document.querySelectorAll('.feed-video').forEach((video) => {
|
||||
video.muted = state.feedMuted;
|
||||
});
|
||||
App.feed.updateMuteButton();
|
||||
};
|
||||
|
||||
App.feed.updateToggleButton = function() {
|
||||
const btn = document.getElementById('mode-toggle-btn');
|
||||
const icon = document.getElementById('mode-toggle-icon');
|
||||
if (!btn || !icon) return;
|
||||
const open = !!state.feedOpen;
|
||||
btn.setAttribute('aria-pressed', open ? 'true' : 'false');
|
||||
const label = open ? 'Back to grid' : 'Switch to Reels view';
|
||||
btn.title = label;
|
||||
icon.alt = label;
|
||||
icon.src = open
|
||||
? 'https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/squares-2x2.svg'
|
||||
: 'https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/device-phone-mobile.svg';
|
||||
};
|
||||
|
||||
App.feed.updateMuteButton = function() {
|
||||
const icon = document.getElementById('feed-mute-icon');
|
||||
if (!icon) return;
|
||||
icon.src = state.feedMuted
|
||||
? 'https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/speaker-x-mark.svg'
|
||||
: 'https://cdn.jsdelivr.net/npm/heroicons@2.0.13/24/outline/speaker-wave.svg';
|
||||
icon.alt = state.feedMuted ? 'Unmute' : 'Mute';
|
||||
};
|
||||
})();
|
||||
@@ -13,7 +13,12 @@ App.state = {
|
||||
playerMode: 'modal',
|
||||
playerHome: null,
|
||||
onFullscreenChange: null,
|
||||
onWebkitEndFullscreen: null
|
||||
onWebkitEndFullscreen: null,
|
||||
loadedVideos: [],
|
||||
feedOpen: false,
|
||||
feedMuted: true,
|
||||
feedRenderedIds: new Set(),
|
||||
feedActiveSlide: null
|
||||
};
|
||||
|
||||
// Local storage keys used across modules.
|
||||
|
||||
@@ -501,6 +501,20 @@ App.ui = App.ui || {};
|
||||
window.closePlayer = App.player.close;
|
||||
window.handleSearch = App.videos.handleSearch;
|
||||
|
||||
const modeToggleBtn = document.getElementById('mode-toggle-btn');
|
||||
if (modeToggleBtn) {
|
||||
modeToggleBtn.onclick = () => {
|
||||
App.feed.toggle();
|
||||
};
|
||||
}
|
||||
|
||||
const feedMuteBtn = document.getElementById('feed-mute-btn');
|
||||
if (feedMuteBtn) {
|
||||
feedMuteBtn.onclick = () => {
|
||||
App.feed.toggleMute();
|
||||
};
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const clearSearchBtn = document.getElementById('search-clear-btn');
|
||||
if (searchInput && clearSearchBtn) {
|
||||
@@ -528,6 +542,9 @@ App.ui = App.ui || {};
|
||||
App.ui.closeDrawers();
|
||||
App.ui.closeInfo();
|
||||
App.videos.closeAllMenus();
|
||||
if (App.feed.isOpen()) {
|
||||
App.feed.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ App.videos = App.videos || {};
|
||||
const favoritesSet = App.favorites.getSet();
|
||||
items.forEach(v => {
|
||||
if (state.renderedVideoIds.has(v.id)) return;
|
||||
state.loadedVideos.push(v);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'video-card';
|
||||
@@ -317,6 +318,9 @@ App.videos = App.videos || {};
|
||||
});
|
||||
|
||||
App.videos.scheduleMasonryLayout();
|
||||
if (App.feed && typeof App.feed.renderSlides === 'function') {
|
||||
App.feed.renderSlides();
|
||||
}
|
||||
App.videos.ensureViewportFilled();
|
||||
};
|
||||
|
||||
@@ -333,8 +337,12 @@ App.videos = App.videos || {};
|
||||
state.currentPage = 1;
|
||||
state.hasNextPage = true;
|
||||
state.renderedVideoIds.clear();
|
||||
state.loadedVideos = [];
|
||||
const grid = document.getElementById('video-grid');
|
||||
if (grid) grid.innerHTML = "";
|
||||
if (App.feed && typeof App.feed.reset === 'function') {
|
||||
App.feed.reset();
|
||||
}
|
||||
App.videos.updateLoadMoreState();
|
||||
App.videos.loadVideos();
|
||||
};
|
||||
@@ -348,8 +356,12 @@ App.videos = App.videos || {};
|
||||
state.currentPage = 1;
|
||||
state.hasNextPage = true;
|
||||
state.renderedVideoIds.clear();
|
||||
state.loadedVideos = [];
|
||||
const grid = document.getElementById('video-grid');
|
||||
if (grid) grid.innerHTML = "";
|
||||
if (App.feed && typeof App.feed.reset === 'function') {
|
||||
App.feed.reset();
|
||||
}
|
||||
App.videos.updateLoadMoreState();
|
||||
App.videos.loadVideos();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user