video end setting
This commit is contained in:
@@ -90,6 +90,13 @@
|
|||||||
<option value="360">360p</option>
|
<option value="360">360p</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="feed-end-select">Reels: On Video End</label>
|
||||||
|
<select id="feed-end-select">
|
||||||
|
<option value="loop">Loop</option>
|
||||||
|
<option value="scroll">Scroll to Next</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="setting-item setting-toggle">
|
<div class="setting-item setting-toggle">
|
||||||
<div class="setting-label-row">
|
<div class="setting-label-row">
|
||||||
<label for="favorites-toggle">Favorites Bar</label>
|
<label for="favorites-toggle">Favorites Bar</label>
|
||||||
|
|||||||
@@ -76,6 +76,22 @@ App.feed = App.feed || {};
|
|||||||
return Math.min(total - 1, Math.max(0, index));
|
return Math.min(total - 1, Math.max(0, index));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// True when the user wants a finished clip to replay; false when it should
|
||||||
|
// auto-advance to the next video. Defaults to looping (see storage).
|
||||||
|
const shouldLoop = function() {
|
||||||
|
return App.storage.getFeedEndBehavior() !== 'scroll';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Smoothly scrolls to the slide after `fromIndex`; the scroll-snap container
|
||||||
|
// fires onScroll, which promotes the new slide to active. No-ops at the end
|
||||||
|
// of the list so the final clip simply stops on its last frame.
|
||||||
|
const advanceToNext = function(fromIndex) {
|
||||||
|
const next = clampIndex(fromIndex + 1);
|
||||||
|
if (next < 0 || next === fromIndex) return;
|
||||||
|
const scroller = getScroller();
|
||||||
|
if (scroller) scroller.scrollTo({ top: next * slideHeight(), behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
// Remembers playback position per video id so scrolling away and back
|
// Remembers playback position per video id so scrolling away and back
|
||||||
// resumes where the user left off. Slides kept in the window are merely
|
// resumes where the user left off. Slides kept in the window are merely
|
||||||
// paused (instant resume); slides whose <video> is torn down to free
|
// paused (instant resume); slides whose <video> is torn down to free
|
||||||
@@ -302,7 +318,7 @@ App.feed = App.feed || {};
|
|||||||
const favKey = App.favorites ? App.favorites.getKey(v) : null;
|
const favKey = App.favorites ? App.favorites.getKey(v) : null;
|
||||||
slide.innerHTML = `
|
slide.innerHTML = `
|
||||||
<img class="feed-poster" src="${v.thumb || ''}" alt="" loading="lazy" decoding="async">
|
<img class="feed-poster" src="${v.thumb || ''}" alt="" loading="lazy" decoding="async">
|
||||||
<video class="feed-video" muted playsinline webkit-playsinline loop preload="none"></video>
|
<video class="feed-video" muted playsinline webkit-playsinline preload="none"></video>
|
||||||
${liveBadge}
|
${liveBadge}
|
||||||
${favKey ? `<button class="favorite-btn feed-fav-btn" type="button" data-fav-key="${favKey}"></button>` : ''}
|
${favKey ? `<button class="favorite-btn feed-fav-btn" type="button" data-fav-key="${favKey}"></button>` : ''}
|
||||||
<div class="feed-info">
|
<div class="feed-info">
|
||||||
@@ -318,7 +334,18 @@ App.feed = App.feed || {};
|
|||||||
`;
|
`;
|
||||||
const poster = slide.querySelector('.feed-poster');
|
const poster = slide.querySelector('.feed-poster');
|
||||||
App.videos.attachNoReferrerRetry(poster);
|
App.videos.attachNoReferrerRetry(poster);
|
||||||
bindTimeline(slide, slide.querySelector('.feed-video'));
|
const slideVideo = slide.querySelector('.feed-video');
|
||||||
|
bindTimeline(slide, slideVideo);
|
||||||
|
|
||||||
|
// On video end, either loop (handled by the `loop` flag, so `ended`
|
||||||
|
// never fires) or auto-scroll to the next clip. We only advance for the
|
||||||
|
// active slide so a preloaded neighbour ending early can't hijack focus.
|
||||||
|
slideVideo.loop = shouldLoop();
|
||||||
|
slideVideo.addEventListener('ended', () => {
|
||||||
|
if (shouldLoop()) return;
|
||||||
|
if (!slide.classList.contains('is-active')) return;
|
||||||
|
advanceToNext(slide._index);
|
||||||
|
});
|
||||||
|
|
||||||
const favBtn = slide.querySelector('.feed-fav-btn');
|
const favBtn = slide.querySelector('.feed-fav-btn');
|
||||||
if (favBtn && App.favorites) {
|
if (favBtn && App.favorites) {
|
||||||
@@ -489,6 +516,16 @@ App.feed = App.feed || {};
|
|||||||
return !!state.feedOpen;
|
return !!state.feedOpen;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Re-applies the on-video-end preference to every rendered slide so toggling
|
||||||
|
// the setting takes effect immediately, without needing to reopen the feed.
|
||||||
|
App.feed.applyEndBehavior = function() {
|
||||||
|
const loop = shouldLoop();
|
||||||
|
slidesByIndex.forEach((slide) => {
|
||||||
|
const video = slide.querySelector('.feed-video');
|
||||||
|
if (video) video.loop = loop;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Called whenever new video JSON is appended (e.g. after a prefetch). Lets
|
// Called whenever new video JSON is appended (e.g. after a prefetch). Lets
|
||||||
// the open feed pick up newly buffered slides and extend its window if the
|
// the open feed pick up newly buffered slides and extend its window if the
|
||||||
// active slide is near the end.
|
// active slide is near the end.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ window.App = window.App || {};
|
|||||||
await App.storage.ensureDefaults();
|
await App.storage.ensureDefaults();
|
||||||
App.ui.applyTheme();
|
App.ui.applyTheme();
|
||||||
App.ui.applyPreferredQuality();
|
App.ui.applyPreferredQuality();
|
||||||
|
App.ui.applyFeedEndBehavior();
|
||||||
App.ui.renderMenu();
|
App.ui.renderMenu();
|
||||||
App.favorites.renderBar();
|
App.favorites.renderBar();
|
||||||
App.ui.bindGlobalHandlers();
|
App.ui.bindGlobalHandlers();
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ App.state = {
|
|||||||
App.constants = {
|
App.constants = {
|
||||||
FAVORITES_KEY: 'favorites',
|
FAVORITES_KEY: 'favorites',
|
||||||
FAVORITES_VISIBILITY_KEY: 'favoritesVisible',
|
FAVORITES_VISIBILITY_KEY: 'favoritesVisible',
|
||||||
PREFERRED_QUALITY_KEY: 'preferredQuality'
|
PREFERRED_QUALITY_KEY: 'preferredQuality',
|
||||||
|
FEED_END_BEHAVIOR_KEY: 'feedEndBehavior'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lazily injects hls.js the first time a stream actually needs it. Sessions
|
// Lazily injects hls.js the first time a stream actually needs it. Sessions
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ App.storage = App.storage || {};
|
|||||||
App.session = App.session || {};
|
App.session = App.session || {};
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY, PREFERRED_QUALITY_KEY } = App.constants;
|
const { FAVORITES_KEY, FAVORITES_VISIBILITY_KEY, PREFERRED_QUALITY_KEY, FEED_END_BEHAVIOR_KEY } = App.constants;
|
||||||
|
|
||||||
// Basic localStorage helpers.
|
// Basic localStorage helpers.
|
||||||
App.storage.getConfig = function() {
|
App.storage.getConfig = function() {
|
||||||
@@ -38,6 +38,16 @@ App.session = App.session || {};
|
|||||||
localStorage.setItem(PREFERRED_QUALITY_KEY, nextQuality);
|
localStorage.setItem(PREFERRED_QUALITY_KEY, nextQuality);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reels/TikTok mode behavior when a video reaches its end: 'loop' replays
|
||||||
|
// the same clip; 'scroll' advances to the next video. Defaults to 'loop'.
|
||||||
|
App.storage.getFeedEndBehavior = function() {
|
||||||
|
return localStorage.getItem(FEED_END_BEHAVIOR_KEY) === 'scroll' ? 'scroll' : 'loop';
|
||||||
|
};
|
||||||
|
|
||||||
|
App.storage.setFeedEndBehavior = function(nextBehavior) {
|
||||||
|
localStorage.setItem(FEED_END_BEHAVIOR_KEY, nextBehavior === 'scroll' ? 'scroll' : 'loop');
|
||||||
|
};
|
||||||
|
|
||||||
App.storage.getServerEntries = function() {
|
App.storage.getServerEntries = function() {
|
||||||
const config = App.storage.getConfig();
|
const config = App.storage.getConfig();
|
||||||
if (!config.servers || !Array.isArray(config.servers)) return [];
|
if (!config.servers || !Array.isArray(config.servers)) return [];
|
||||||
@@ -187,6 +197,9 @@ App.session = App.session || {};
|
|||||||
if (!localStorage.getItem(FAVORITES_VISIBILITY_KEY)) {
|
if (!localStorage.getItem(FAVORITES_VISIBILITY_KEY)) {
|
||||||
localStorage.setItem(FAVORITES_VISIBILITY_KEY, 'true');
|
localStorage.setItem(FAVORITES_VISIBILITY_KEY, 'true');
|
||||||
}
|
}
|
||||||
|
if (!localStorage.getItem(FEED_END_BEHAVIOR_KEY)) {
|
||||||
|
localStorage.setItem(FEED_END_BEHAVIOR_KEY, 'loop');
|
||||||
|
}
|
||||||
await App.storage.initializeServerStatus();
|
await App.storage.initializeServerStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ App.ui = App.ui || {};
|
|||||||
if (select) select.value = App.storage.getPreferredQuality();
|
if (select) select.value = App.storage.getPreferredQuality();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
App.ui.applyFeedEndBehavior = function() {
|
||||||
|
const select = document.getElementById('feed-end-select');
|
||||||
|
if (select) select.value = App.storage.getFeedEndBehavior();
|
||||||
|
};
|
||||||
|
|
||||||
// Toast helper for playback + network errors.
|
// Toast helper for playback + network errors.
|
||||||
App.ui.showError = function(message) {
|
App.ui.showError = function(message) {
|
||||||
const toast = document.getElementById('error-toast');
|
const toast = document.getElementById('error-toast');
|
||||||
@@ -281,6 +286,17 @@ App.ui = App.ui || {};
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const feedEndSelect = document.getElementById('feed-end-select');
|
||||||
|
if (feedEndSelect) {
|
||||||
|
feedEndSelect.value = App.storage.getFeedEndBehavior();
|
||||||
|
feedEndSelect.onchange = () => {
|
||||||
|
App.storage.setFeedEndBehavior(feedEndSelect.value);
|
||||||
|
if (App.feed && typeof App.feed.applyEndBehavior === 'function') {
|
||||||
|
App.feed.applyEndBehavior();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (favoritesToggle) {
|
if (favoritesToggle) {
|
||||||
favoritesToggle.checked = App.favorites.isVisible();
|
favoritesToggle.checked = App.favorites.isVisible();
|
||||||
favoritesToggle.onchange = () => {
|
favoritesToggle.onchange = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user