Add playbackRate video option, improve menu widget options
-Add option for playback rate (speed) -Add hotkeys for changing playback rate ( , and . ) -Add labeled number input widget -Inherit labeled widget types from base class -Add option for onchange to above base class -Clamp numbers when setting numeric values on videos -Add some helper functions to video elements -Remove bPropagateOnChange, roll functionality into forEachElement delegate -Add widgetType parameter to inform the widget type to be created -Removed unnecessary and incorrect comments
This commit is contained in:
@@ -6,7 +6,7 @@ import { getPngMetadata } from "/scripts/pnginfo.js";
|
||||
|
||||
import {
|
||||
getMaxZIndex, createDarkContainer, copyToClipboard,
|
||||
isValid, getCurrentSecondsFromEpoch
|
||||
isValid, getCurrentSecondsFromEpoch, clamp
|
||||
} from "../common/Utilities.js";
|
||||
|
||||
import { getLastMousePosition, isPointerDown } from "../common/EventManager.js";
|
||||
@@ -574,8 +574,27 @@ export async function createImageElementFromFileInfo(fileInfo) {
|
||||
}
|
||||
});
|
||||
|
||||
imageElement.forceLoad = function () {
|
||||
img.forceLoad = function () {
|
||||
img.src = img.dataSrc;
|
||||
|
||||
if (bIsVideoFormat) {
|
||||
img.setPlaybackRate(setting_VideoPlaybackOptions.value.defaultPlaybackRate); // This gets reset when src is reset
|
||||
}
|
||||
}
|
||||
|
||||
img.initVideo = function () {
|
||||
|
||||
img.type = fileInfo.file?.format || undefined;
|
||||
img.autoplay = false; // Start false, will autoplay via observer
|
||||
img.loop = setting_VideoPlaybackOptions.value.loop;
|
||||
img.controls = setting_VideoPlaybackOptions.value.controls;
|
||||
img.muted = setting_VideoPlaybackOptions.value.muted;
|
||||
img.setVolume(setting_VideoPlaybackOptions.value.defaultVolume);
|
||||
img.setPlaybackRate(setting_VideoPlaybackOptions.value.defaultPlaybackRate);
|
||||
}
|
||||
|
||||
imageElement.forceLoad = function () {
|
||||
img.forceLoad();
|
||||
}
|
||||
|
||||
if (fileInfo.bShouldForceLoad) {
|
||||
@@ -654,15 +673,6 @@ export async function createImageElementFromFileInfo(fileInfo) {
|
||||
|
||||
if (bIsVideoFormat) {
|
||||
|
||||
img.type = fileInfo.file?.format || undefined;
|
||||
img.autoplay = false; // Start false, will autoplay via observer
|
||||
img.loop = setting_VideoPlaybackOptions.value.loop;
|
||||
img.controls = setting_VideoPlaybackOptions.value.controls;
|
||||
img.muted = setting_VideoPlaybackOptions.value.muted;
|
||||
img.volume = setting_VideoPlaybackOptions.value.defaultVolume / 100; // Slider is 1-100, volume is 0-1
|
||||
|
||||
imageElement.bIsVideoFormat = bIsVideoFormat;
|
||||
|
||||
img.togglePlayback = async function () {
|
||||
if (!img.pause || !img.play || !('paused' in img)) { return; }
|
||||
|
||||
@@ -679,6 +689,10 @@ export async function createImageElementFromFileInfo(fileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
img.setVolume = function (percentage) {
|
||||
img.volume = clamp(percentage / 100, 0.0, 1.0); // Slider is 1-100, volume is 0-1
|
||||
}
|
||||
|
||||
img.toggleFullscreen = function () {
|
||||
if (!document.fullscreenElement) {
|
||||
img.requestFullscreen();
|
||||
@@ -687,15 +701,19 @@ export async function createImageElementFromFileInfo(fileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
img.setPlaybackRate = function (rate) {
|
||||
img.playbackRate = clamp(rate, 0.05, 100.00);
|
||||
}
|
||||
|
||||
img.seekVideo = function (delta) {
|
||||
if (!img.duration || !img.currentTime) { return; }
|
||||
|
||||
const maxTime = img.duration;
|
||||
const currentTime = img.currentTime;
|
||||
const seekStep = Math.min(10, Math.max(1, maxTime / 100)); // Seek multiplier
|
||||
const seekStep = clamp(maxTime / 100, 1, 10); // Seek multiplier: 1 sec min / 1% of video / 10 sec max
|
||||
|
||||
let newTime = currentTime + (delta * seekStep);
|
||||
newTime = Math.max(0, Math.min(maxTime, newTime)); // Clamp within valid range
|
||||
newTime = clamp(newTime, 0, maxTime); // Clamp within valid range
|
||||
|
||||
img.currentTime = newTime; // Seek the video to the new time
|
||||
};
|
||||
@@ -714,6 +732,10 @@ export async function createImageElementFromFileInfo(fileInfo) {
|
||||
img.seekVideo(scrollDelta);
|
||||
}
|
||||
});
|
||||
|
||||
img.initVideo();
|
||||
|
||||
imageElement.bIsVideoFormat = bIsVideoFormat;
|
||||
}
|
||||
|
||||
aElement.appendChild(img);
|
||||
|
||||
@@ -63,7 +63,7 @@ document.addEventListener("keydown", async (event) => {
|
||||
if (elementUnderPointer && elementUnderPointer.toggleMute) {
|
||||
event.preventDefault();
|
||||
|
||||
elementUnderPointer.toggleMute()
|
||||
elementUnderPointer.toggleMute();
|
||||
}
|
||||
} else if (event.key == 'f') { // Toggle fullscreen
|
||||
const elementUnderPointer = getElementUnderPointer();
|
||||
@@ -71,7 +71,23 @@ document.addEventListener("keydown", async (event) => {
|
||||
if (elementUnderPointer && elementUnderPointer.toggleFullscreen) {
|
||||
event.preventDefault();
|
||||
|
||||
elementUnderPointer.toggleFullscreen()
|
||||
elementUnderPointer.toggleFullscreen();
|
||||
}
|
||||
} else if (event.key == ',') { // Decrease playback rate
|
||||
const elementUnderPointer = getElementUnderPointer();
|
||||
|
||||
if (elementUnderPointer && elementUnderPointer.playbackRate && elementUnderPointer.setPlaybackRate) {
|
||||
event.preventDefault();
|
||||
|
||||
elementUnderPointer.setPlaybackRate(elementUnderPointer.playbackRate - 0.05);
|
||||
}
|
||||
} else if (event.key == '.') { // Increase playback rate
|
||||
const elementUnderPointer = getElementUnderPointer();
|
||||
|
||||
if (elementUnderPointer && elementUnderPointer.playbackRate && elementUnderPointer.setPlaybackRate) {
|
||||
event.preventDefault();
|
||||
|
||||
elementUnderPointer.setPlaybackRate(elementUnderPointer.playbackRate + 0.05);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ const imageAndVideoObserver = new IntersectionObserver((entries) => {
|
||||
// Check if the video is intersecting with the viewport
|
||||
if (entry.isIntersecting) {
|
||||
if (!element.src || element.src === '') {
|
||||
element.src = element.dataSrc;
|
||||
element.forceLoad();
|
||||
|
||||
if (element.tagName !== 'VIDEO') {
|
||||
unobserveVisualElement(element);
|
||||
|
||||
@@ -323,17 +323,10 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
|
||||
|
||||
const infos = new info_VideoPlaybackOptions();
|
||||
|
||||
function setGenericSettingOnVisualElementsInImageList(propertyName, propertyValue, info) {
|
||||
function callForEachCallbackOnEachElementInImageList(propertyName, propertyValue, info) {
|
||||
for (let child of getImageListChildren()) {
|
||||
for (let element of getVideoElements(child)) {
|
||||
|
||||
if (info.bPropagateOnChange) {
|
||||
element[propertyName] = propertyValue;
|
||||
}
|
||||
|
||||
if (info.forEachElement) {
|
||||
info.forEachElement(element, propertyValue);
|
||||
}
|
||||
info.forEachElement(element, propertyName, propertyValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,8 +338,8 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
|
||||
|
||||
// If requested, set similarly named properties on image list
|
||||
const info = infos[propertyName];
|
||||
if (info.bPropagateOnChange || info.forEachElement) {
|
||||
setGenericSettingOnVisualElementsInImageList(propertyName, newValue, info);
|
||||
if (info.forEachElement) {
|
||||
callForEachCallbackOnEachElementInImageList(propertyName, newValue, info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +347,9 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
|
||||
Object.entries(new options_VideoPlayback()).forEach(([propertyName, propertyValue]) => {
|
||||
// In case we're using an old serialization, use the serialized value or default
|
||||
const propertyValueToUse = propertyName in setting_VideoPlaybackOptions.value ? setting_VideoPlaybackOptions.value[propertyName] : propertyValue;
|
||||
const info = infos[propertyName];
|
||||
let widget;
|
||||
if (typeof propertyValueToUse === 'boolean') {
|
||||
if (info.widgetType === 'checkbox') {
|
||||
let options = new options_LabeledCheckboxToggle();
|
||||
options.id = `VideoPlaybackOptions.${propertyName}`
|
||||
options.checked = propertyValueToUse;
|
||||
@@ -366,18 +360,35 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
|
||||
oninput(propertyName, newValue);
|
||||
}
|
||||
widget = createLabeledCheckboxToggle(options);
|
||||
} else if (typeof propertyValueToUse === 'number') {
|
||||
} else if (info.widgetType === 'range') {
|
||||
let options = new options_LabeledSliderRange();
|
||||
options.id = `VideoPlaybackOptions.${propertyName}`
|
||||
options.value = propertyValueToUse;
|
||||
options.labelTextContent = propertyName;
|
||||
options.bIncludeValueLabel = false;
|
||||
options.min = info?.min ? info.min : options.min;
|
||||
options.max = info?.max ? info.max : options.max;
|
||||
options.step = info?.step ? info.step : options.step;
|
||||
options.oninput = (e) => {
|
||||
// This config setting is a class instance, so we need to take a few extra steps to serialize it
|
||||
const newValue = e.target.valueAsNumber;
|
||||
oninput(propertyName, newValue);
|
||||
}
|
||||
widget = createLabeledSliderRange(options);
|
||||
} else if (info.widgetType === 'number') {
|
||||
let options = new options_LabeledNumberInput();
|
||||
options.id = `VideoPlaybackOptions.${propertyName}`
|
||||
options.value = propertyValueToUse;
|
||||
options.labelTextContent = propertyName;
|
||||
options.min = info?.min ? info.min : options.min;
|
||||
options.max = info?.max ? info.max : options.max;
|
||||
options.step = info?.step ? info.step : options.step;
|
||||
options.onchange = (e) => { // Use onchange here because the user can type a value in, onchange requires commit
|
||||
// This config setting is a class instance, so we need to take a few extra steps to serialize it
|
||||
const newValue = e.target.valueAsNumber;
|
||||
oninput(propertyName, newValue);
|
||||
}
|
||||
widget = createLabeledNumberInput(options);
|
||||
}
|
||||
widget.title = infos[propertyName]?.tooltip || '';
|
||||
menu.appendChild(widget);
|
||||
@@ -394,17 +405,33 @@ export function createVideoPlaybackOptionsFlyout() {
|
||||
return flyout;
|
||||
}
|
||||
|
||||
export class options_LabeledSliderRange {
|
||||
class options_BaseLabeledWidget {
|
||||
id = undefined;
|
||||
labelTextContent = undefined;
|
||||
oninput = undefined;
|
||||
onchange = undefined;
|
||||
|
||||
bindEvents(widget) {
|
||||
if (this.oninput) {
|
||||
widget.oninput = this.oninput;
|
||||
}
|
||||
|
||||
if (this.onchange) {
|
||||
widget.onchange = this.onchange;
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
export class options_LabeledSliderRange extends options_BaseLabeledWidget {
|
||||
bIncludeValueLabel = true;
|
||||
bPrependValueLabel = false;
|
||||
valueLabelFractionalDigits = 0;
|
||||
id = undefined;
|
||||
value = 0;
|
||||
min = 0;
|
||||
max = 100
|
||||
step = 1;
|
||||
oninput = undefined;
|
||||
}
|
||||
|
||||
export function createLabeledSliderRange(options = new options_LabeledSliderRange()) {
|
||||
@@ -441,10 +468,11 @@ export function createLabeledSliderRange(options = new options_LabeledSliderRang
|
||||
value: options.value,
|
||||
min: options.min,
|
||||
max: options.max,
|
||||
step: options.step,
|
||||
oninput: options.oninput
|
||||
step: options.step
|
||||
});
|
||||
|
||||
options.bindEvents(MainElement);
|
||||
|
||||
let OuterElement = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
@@ -467,11 +495,42 @@ export function createLabeledSliderRange(options = new options_LabeledSliderRang
|
||||
return OuterElement;
|
||||
}
|
||||
|
||||
export class options_LabeledCheckboxToggle {
|
||||
labelTextContent = undefined;
|
||||
id = undefined;
|
||||
export class options_LabeledNumberInput extends options_BaseLabeledWidget {
|
||||
value = 0;
|
||||
min = 0;
|
||||
max = 100
|
||||
step = 1;
|
||||
}
|
||||
|
||||
export function createLabeledNumberInput(options = new options_LabeledNumberInput()) {
|
||||
|
||||
let MainElement = $el('input', {
|
||||
id: options.id,
|
||||
type: 'number',
|
||||
value: options.value,
|
||||
min: options.min,
|
||||
max: options.max,
|
||||
step: options.step
|
||||
});
|
||||
|
||||
options.bindEvents(MainElement);
|
||||
|
||||
let OuterElement = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5%'
|
||||
}
|
||||
});
|
||||
|
||||
OuterElement.appendChild($el('label', { textContent: options.labelTextContent }));
|
||||
OuterElement.appendChild(MainElement);
|
||||
|
||||
return OuterElement;
|
||||
}
|
||||
|
||||
export class options_LabeledCheckboxToggle extends options_BaseLabeledWidget {
|
||||
checked = false;
|
||||
oninput = undefined;
|
||||
}
|
||||
|
||||
export function createLabeledCheckboxToggle(options = new options_LabeledCheckboxToggle()) {
|
||||
@@ -479,10 +538,11 @@ export function createLabeledCheckboxToggle(options = new options_LabeledCheckbo
|
||||
let MainElement = $el('input', {
|
||||
id: options.id,
|
||||
type: 'checkbox',
|
||||
checked: options.checked,
|
||||
oninput: options.oninput
|
||||
checked: options.checked
|
||||
});
|
||||
|
||||
options.bindEvents(MainElement);
|
||||
|
||||
let OuterElement = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
export class options_VideoPlayback {
|
||||
autoplay = false; loop = true; controls = true; muted = true; useWheelSeek = true; invertWheelSeek = true; defaultVolume = 50;
|
||||
autoplay = false; loop = true; controls = true; muted = true; useWheelSeek = true; invertWheelSeek = true; defaultVolume = 50; defaultPlaybackRate = 1.00;
|
||||
};
|
||||
|
||||
export class info_VideoPlaybackOptions {
|
||||
autoplay = { tooltip: 'Should the videos automatically play as they scroll into view? Does not apply to animated images, only video types.',
|
||||
bPropagateOnChange: true, forEachElement: forEachElement_Autoplay };
|
||||
loop = { tooltip: 'Should the videos automatically play again when the end is reached?', bPropagateOnChange: true, forEachElement: null };
|
||||
controls = { tooltip: 'Should controls such as play/pause, volume, etc be displayed on the video?', bPropagateOnChange: true, forEachElement: null };
|
||||
muted = { tooltip: 'Should videos play without audio?', bPropagateOnChange: true, forEachElement: null };
|
||||
useWheelSeek = { tooltip: 'Should videos seek when mousing over a video and using the scroll wheel?', bPropagateOnChange: false, forEachElement: null };
|
||||
forEachElement: forEachElement_Autoplay, widgetType: 'checkbox' };
|
||||
loop = { tooltip: 'Should the videos automatically play again when the end is reached?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
|
||||
controls = { tooltip: 'Should controls such as play/pause, volume, etc be displayed on the video?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
|
||||
muted = { tooltip: 'Should videos play without audio?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
|
||||
useWheelSeek = { tooltip: 'Should videos seek when mousing over a video and using the scroll wheel?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
|
||||
invertWheelSeek = { tooltip: 'Should scroll wheel seeking be inverted? (If true, scrolling up will seek forward. If false, will seek backward)',
|
||||
bPropagateOnChange: false, forEachElement: null };
|
||||
defaultVolume = { tooltip: 'The default volume at which videos should play if not muted.', bPropagateOnChange: false, forEachElement: forEachElement_Volume };
|
||||
forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
|
||||
defaultVolume = { tooltip: 'The default volume at which videos should play if not muted', forEachElement: forEachElement_Volume, widgetType: 'range' };
|
||||
defaultPlaybackRate = { tooltip: 'The default rate at which videos should play', forEachElement: forEachElement_playbackRate, widgetType: 'number', min: 0.01, max: 100.00, step: 0.01 };
|
||||
};
|
||||
|
||||
function forEachElement_Autoplay(element, propertyValue) {
|
||||
function forEachElement_Autoplay(element, propertyName, propertyValue) {
|
||||
// Pause anything currently playing if autoplay is false
|
||||
// We don't want to play just anything, we'll let observer take care of that
|
||||
if (!propertyValue && element.pause && 'paused' in element && !element.paused) {
|
||||
@@ -22,10 +23,18 @@ function forEachElement_Autoplay(element, propertyValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function forEachElement_Volume(element, propertyValue) {
|
||||
// Pause anything currently playing if autoplay is false
|
||||
// We don't want to play just anything, we'll let observer take care of that
|
||||
if (propertyValue && 'volume' in element) {
|
||||
element.volume = propertyValue / 100; // Slider is 1-100, volume is 0-1
|
||||
function forEachElement_genericPropagatation(element, propertyName, propertyValue) {
|
||||
element[propertyName] = propertyValue;
|
||||
}
|
||||
|
||||
function forEachElement_Volume(element, propertyName, propertyValue) {
|
||||
if (propertyValue && element.setVolume) {
|
||||
element.setVolume(propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
function forEachElement_playbackRate(element, propertyName, propertyValue) {
|
||||
if (propertyValue && element.setPlaybackRate) {
|
||||
element.setPlaybackRate(propertyValue);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user