Audio posts

Not yet available int the gallery. Ill add that if someone asks for it.
This commit is contained in:
Tuxedo Takodachi 2023-06-24 17:52:48 +02:00
parent e4236c62c8
commit 7d6dd7c653
10 changed files with 1510 additions and 1188 deletions

View File

@ -725,7 +725,11 @@ div.boardTitle {
'Volume in New Tab': [ 'Volume in New Tab': [
true, true,
`Apply ${meta.name} mute and volume settings to videos opened in their own tabs.` `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.`
] ],
'Enable sound posts': [
true,
'Enable loading audio from [sound=] file names. This audio is fetched from third parties.'
],
}, },
'Menu': { 'Menu': {
@ -3378,391 +3382,476 @@ https://*.hcaptcha.com
} }
}; };
/* const Audio = {
* decaffeinate suggestions: /** Add event listeners for videos with audio from a third party */
* DS102: Remove unnecessary code created because of implicit returns setupSync(video, audio) {
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md video.addEventListener('playing', () => {
*/ audio.currentTime = video.currentTime;
var ImageExpand = { audio.play();
init() { });
if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { return; } video.addEventListener('pause', () => {
audio.pause();
});
video.addEventListener('seeked', () => {
audio.currentTime = video.currentTime;
});
video.addEventListener('ratechange', () => {
audio.currentTime = video.currentTime;
audio.playbackRate = video.playbackRate;
});
video.addEventListener('waiting', () => {
audio.currentTime = video.currentTime;
audio.pause();
});
audio.addEventListener('canplay', () => {
if (audio.currentTime < .1)
video.currentTime = 0;
}, { once: true });
},
setupAudioSlider(video, audio) {
const container = document.createElement('span');
// \u00A0 is non breaking space
container.appendChild(document.createTextNode('🔊︎\u00A0'));
const control = document.createElement('input');
control.type = 'range';
control.max = '1';
control.step = '0.01';
control.valueAsNumber = audio.volume;
control.addEventListener('input', () => {
audio.volume = control.valueAsNumber;
});
container.appendChild(control);
const downloadLink = document.createElement('a');
downloadLink.href = audio.src;
downloadLink.download = '';
downloadLink.target = '_blank';
downloadLink.textContent = '\u00A0📥';
container.appendChild(downloadLink);
return container;
},
};
this.EAI = $$1.el('a', { /*
className: 'expand-all-shortcut', * decaffeinate suggestions:
textContent: '', * DS102: Remove unnecessary code created because of implicit returns
title: 'Expand All Images', * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
href: 'javascript:;' */
} var ImageExpand = {
); init() {
if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) {
$$1.on(this.EAI, 'click', this.cb.toggleAll); return;
Header$1.addShortcut('expand-all', this.EAI, 520); }
$$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos); this.EAI = $$1.el('a', {
this.videoControls = $$1.el('span', {className: 'video-controls'}); className: 'expand-all-shortcut',
$$1.extend(this.videoControls, {innerHTML: " <a href=\"javascript:;\" title=\"You can also contract the video by dragging it to the left.\">contract</a>"}); textContent: '',
title: 'Expand All Images',
return Callbacks.Post.push({ href: 'javascript:;'
name: 'Image Expansion', });
cb: this.node $$1.on(this.EAI, 'click', this.cb.toggleAll);
}); Header$1.addShortcut('expand-all', this.EAI, 520);
}, $$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos);
this.videoControls = $$1.el('span', { className: 'video-controls' });
node() { $$1.extend(this.videoControls, { innerHTML: " <a href=\"javascript:;\" title=\"You can also contract the video by dragging it to the left.\">contract</a>" });
if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; } return Callbacks.Post.push({
$$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle); name: 'Image Expansion',
cb: this.node
if (this.isClone) { });
if (this.file.isExpanding) { },
// If we clone a post where the image is still loading, node() {
// make it loading in the clone too. if (!this.file || (!this.file.isImage && !this.file.isVideo)) {
ImageExpand.contract(this); return;
return ImageExpand.expand(this); }
$$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
} else if (this.file.isExpanded && this.file.isVideo) { if (this.isClone) {
Volume.setup(this.file.fullImage); if (this.file.isExpanding) {
ImageExpand.setupVideoCB(this); // If we clone a post where the image is still loading,
return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls); // make it loading in the clone too.
} ImageExpand.contract(this);
return ImageExpand.expand(this);
} else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote && }
(Conf['Expand spoilers'] || !this.file.isSpoiler) && else if (this.file.isExpanded && this.file.isVideo) {
(Conf['Expand videos'] || !this.file.isVideo)) { Volume.setup(this.file.fullImage);
return ImageExpand.expand(this); ImageExpand.setupVideoCB(this);
} return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls);
}, }
}
cb: { else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote &&
toggle(e) { (Conf['Expand spoilers'] || !this.file.isSpoiler) &&
if ($$1.modifiedClick(e)) { return; } (Conf['Expand videos'] || !this.file.isVideo)) {
const post = Get$1.postFromNode(this); return ImageExpand.expand(this);
const {file} = post; }
if (file.isExpanded && ImageCommon.onControls(e)) { return; } },
e.preventDefault(); cb: {
if (!Conf['Autoplay'] && file.fullImage?.paused) { toggle(e) {
return file.fullImage.play(); if ($$1.modifiedClick(e)) {
} else { return;
return ImageExpand.toggle(post); }
} const post = Get$1.postFromNode(this);
}, const { file } = post;
if (file.isExpanded && ImageCommon.onControls(e)) {
toggleAll() { return;
let func; }
$$1.event('CloseMenu'); e.preventDefault();
const threadRoot = Nav.getThread(); if (!Conf['Autoplay'] && file.fullImage?.paused) {
const toggle = function(post) { return file.fullImage.play();
const {file} = post; }
if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) { return; } else {
if (ImageExpand.on && return ImageExpand.toggle(post);
((!Conf['Expand spoilers'] && file.isSpoiler) || }
(!Conf['Expand videos'] && file.isVideo) || },
(Conf['Expand from here'] && (Header$1.getTopOf(file.thumb) < 0)) || toggleAll() {
(Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) { let func;
return; $$1.event('CloseMenu');
} const threadRoot = Nav.getThread();
return $$1.queueTask(func, post); const toggle = function (post) {
}; const { file } = post;
if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) {
if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) { return;
ImageExpand.EAI.className = 'contract-all-shortcut'; }
ImageExpand.EAI.title = 'Contract All Images'; if (ImageExpand.on &&
ImageExpand.EAI.textContent = ''; ((!Conf['Expand spoilers'] && file.isSpoiler) ||
func = ImageExpand.expand; (!Conf['Expand videos'] && file.isVideo) ||
} else { (Conf['Expand from here'] && (Header$1.getTopOf(file.thumb) < 0)) ||
ImageExpand.EAI.className = 'expand-all-shortcut'; (Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) {
ImageExpand.EAI.title = 'Expand All Images'; return;
ImageExpand.EAI.textContent = ''; }
func = ImageExpand.contract; return $$1.queueTask(func, post);
} };
if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) {
return g.posts.forEach(function(post) { ImageExpand.EAI.className = 'contract-all-shortcut';
for (post of [post, ...post.clones]) { toggle(post); } ImageExpand.EAI.title = 'Contract All Images';
}); ImageExpand.EAI.textContent = '';
}, func = ImageExpand.expand;
}
playVideos() { else {
return g.posts.forEach(function(post) { ImageExpand.EAI.className = 'expand-all-shortcut';
for (post of [post, ...post.clones]) { ImageExpand.EAI.title = 'Expand All Images';
var {file} = post; ImageExpand.EAI.textContent = '';
if (!file || !file.isVideo || !file.isExpanded) { continue; } func = ImageExpand.contract;
}
var video = file.fullImage; return g.posts.forEach(function (post) {
var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video); for (post of [post, ...post.clones]) {
if (visible && file.wasPlaying) { toggle(post);
delete file.wasPlaying; }
video.play(); });
} else if (!visible && !video.paused) { },
file.wasPlaying = true; playVideos() {
video.pause(); return g.posts.forEach(function (post) {
} for (post of [post, ...post.clones]) {
} var { file } = post;
}); if (!file || !file.isVideo || !file.isExpanded) {
}, continue;
}
setFitness() { var video = file.fullImage;
return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-')); var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video);
} if (visible && file.wasPlaying) {
}, delete file.wasPlaying;
video.play();
toggle(post) { }
if (!post.file.isExpanding && !post.file.isExpanded) { else if (!visible && !video.paused) {
post.file.scrollIntoView = Conf['Scroll into view']; file.wasPlaying = true;
ImageExpand.expand(post); video.pause();
return; }
} }
});
ImageExpand.contract(post); },
setFitness() {
if (Conf['Advance on contract']) { return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-'));
let next = post.nodes.root; }
while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) { },
if (!$$1('.stub', next) && (next.offsetHeight !== 0)) { break; } toggle(post) {
} if (!post.file.isExpanding && !post.file.isExpanded) {
if (next) { post.file.scrollIntoView = Conf['Scroll into view'];
return Header$1.scrollTo(next); ImageExpand.expand(post);
} return;
} }
}, ImageExpand.contract(post);
if (Conf['Advance on contract']) {
contract(post) { let next = post.nodes.root;
let bottom, el, oldHeight, scrollY; while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) {
const {file} = post; if (!$$1('.stub', next) && (next.offsetHeight !== 0)) {
break;
if (el = file.fullImage) { }
const top = Header$1.getTopOf(el); }
bottom = top + el.getBoundingClientRect().height; if (next) {
oldHeight = d$1.body.clientHeight; return Header$1.scrollTo(next);
({scrollY} = window); }
} }
},
$$1.rmClass(post.nodes.root, 'expanded-image'); contract(post) {
$$1.rmClass(file.thumb, 'expanding'); let bottom, el, oldHeight, scrollY;
$$1.rm(file.videoControls); const { file } = post;
file.thumbLink.href = file.url; if (el = file.fullImage) {
file.thumbLink.target = '_blank'; const top = Header$1.getTopOf(el);
for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) { bottom = top + el.getBoundingClientRect().height;
delete file[x]; oldHeight = d$1.body.clientHeight;
} ({ scrollY } = window);
}
if (!el) { return; } $$1.rmClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding');
if (doc$1.contains(el)) { $$1.rm(file.videoControls);
if (bottom <= 0) { file.thumbLink.href = file.url;
// For images entirely above us, scroll to remain in place. file.thumbLink.target = '_blank';
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) {
} else { delete file[x];
// For images not above us that would be moved above us, scroll to the thumbnail. }
Header$1.scrollToIfNeeded(post.nodes.root); if (!el) {
} return;
if (window.scrollX > 0) { }
// If we have scrolled right viewing an expanded image, return to the left. if (doc$1.contains(el)) {
window.scrollBy(-window.scrollX, 0); if (bottom <= 0) {
} // For images entirely above us, scroll to remain in place.
} window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
}
$$1.off(el, 'error', ImageExpand.error); else {
ImageCommon.pushCache(el); // For images not above us that would be moved above us, scroll to the thumbnail.
if (file.isVideo) { Header$1.scrollToIfNeeded(post.nodes.root);
ImageCommon.pause(el); }
for (var eventName in ImageExpand.videoCB) { if (window.scrollX > 0) {
var cb = ImageExpand.videoCB[eventName]; // If we have scrolled right viewing an expanded image, return to the left.
$$1.off(el, eventName, cb); window.scrollBy(-window.scrollX, 0);
} }
} }
if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); } $$1.off(el, 'error', ImageExpand.error);
delete file.fullImage; ImageCommon.pushCache(el);
return $$1.queueTask(function() { if (file.isVideo) {
// XXX Work around Chrome/Chromium not firing mouseover on the thumbnail. ImageCommon.pause(el);
if (file.isExpanding || file.isExpanded) { return; } for (var eventName in ImageExpand.videoCB) {
$$1.rmClass(el, 'full-image'); var cb = ImageExpand.videoCB[eventName];
if (el.id) { return; } $$1.off(el, eventName, cb);
return $$1.rm(el); }
}); }
}, if (Conf['Restart when Opened']) {
ImageCommon.rewind(file.thumb);
expand(post, src) { }
// Do not expand images of hidden/filtered replies, or already expanded pictures. delete file.fullImage;
let el; $$1.queueTask(function () {
const {file} = post; // XXX Work around Chrome/Chromium not firing mouseover on the thumbnail.
const {thumb, thumbLink, isVideo} = file; if (file.isExpanding || file.isExpanded) {
if (post.isHidden || file.isExpanding || file.isExpanded) { return; } return;
}
$$1.addClass(thumb, 'expanding'); $$1.rmClass(el, 'full-image');
file.isExpanding = true; if (el.id) {
return;
if (file.fullImage) { }
el = file.fullImage; return $$1.rm(el);
} else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) { });
el = (file.fullImage = ImageCommon.popCache()); if (file.audio) {
$$1.on(el, 'error', ImageExpand.error); file.audio.remove();
if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { ImageCommon.rewind(el); } delete file.audio;
el.removeAttribute('id'); if (file.audioSlider) {
} else { file.audioSlider.remove();
el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img'))); delete file.audioSlider;
el.dataset.fileID = `${post.fullID}.${file.index}`; }
$$1.on(el, 'error', ImageExpand.error); }
el.src = src || file.url; },
} expand(post, src) {
const { file } = post;
el.className = 'full-image'; const { thumb, thumbLink, isVideo } = file;
$$1.after(thumb, el); // Do not expand images of hidden/filtered replies, or already expanded pictures.
if (post.isHidden || file.isExpanding || file.isExpanded) {
if (isVideo) { return;
// add contract link to file info }
if (!file.videoControls) { let el;
file.videoControls = ImageExpand.videoControls.cloneNode(true); $$1.addClass(thumb, 'expanding');
$$1.add(file.text, file.videoControls); file.isExpanding = true;
} if (file.fullImage) {
el = file.fullImage;
// disable link to file so native controls can work }
thumbLink.removeAttribute('href'); else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) {
thumbLink.removeAttribute('target'); el = (file.fullImage = ImageCommon.popCache());
$$1.on(el, 'error', ImageExpand.error);
el.loop = true; if (Conf['Restart when Opened'] && (el.id !== 'ihover')) {
Volume.setup(el); ImageCommon.rewind(el);
ImageExpand.setupVideoCB(post); }
} el.removeAttribute('id');
}
if (!isVideo) { else {
return $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post)); el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img')));
} else if (el.readyState >= el.HAVE_METADATA) { el.dataset.fileID = `${post.fullID}.${file.index}`;
return ImageExpand.completeExpand(post); $$1.on(el, 'error', ImageExpand.error);
} else { el.src = src || file.url;
return $$1.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post)); }
} el.className = 'full-image';
}, $$1.after(thumb, el);
if (isVideo) {
completeExpand(post) { // add contract link to file info
const {file} = post; if (!file.videoControls) {
if (!file.isExpanding) { return; } // contracted before the image loaded file.videoControls = ImageExpand.videoControls.cloneNode(true);
$$1.add(file.text, file.videoControls);
const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; }
const oldHeight = d$1.body.clientHeight; // disable link to file so native controls can work
const {scrollY} = window; thumbLink.removeAttribute('href');
thumbLink.removeAttribute('target');
$$1.addClass(post.nodes.root, 'expanded-image'); el.loop = true;
$$1.rmClass(file.thumb, 'expanding'); Volume.setup(el);
file.isExpanded = true; ImageExpand.setupVideoCB(post);
delete file.isExpanding; }
if (!isVideo) {
// Scroll to keep our place in the thread when images are expanded above us. $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
if (doc$1.contains(post.nodes.root) && (bottom <= 0)) { }
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); else if (el.readyState >= el.HAVE_METADATA) {
} ImageExpand.completeExpand(post);
}
// Scroll to display full image. else {
if (file.scrollIntoView) { $$1.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post));
delete file.scrollIntoView; }
const imageBottom = Math.min(doc$1.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header$1.getBottomOf(file.fullImage)); if (Conf['Enable sound posts'] && Conf['Allow Sound']) {
if (imageBottom < 0) { const soundUrlMatch = file.name.match(/\[sound=([^\]]+)]/);
window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage))); if (soundUrlMatch) {
} let src = decodeURIComponent(soundUrlMatch[1]);
} if (!src.startsWith('http'))
src = `https://${src}`;
if (file.isVideo) { const audioEl = $$1.el('audio', { src });
return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']); Volume.setup(audioEl);
} if (isVideo) {
}, Audio.setupSync(el, audioEl);
if (Conf['Show Controls']) {
setupVideo(post, playing, controls) { file.audioSlider = Audio.setupAudioSlider(el, audioEl);
const {fullImage} = post.file; $$1.after(el.parentElement, file.audioSlider);
if (!playing) { }
fullImage.controls = controls; }
return; else {
} audioEl.controls = Conf['Show Controls'];
fullImage.controls = false; audioEl.autoplay = Conf['Autoplay'];
$$1.asap((() => doc$1.contains(fullImage)), function() { }
if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) { $$1.after(el, audioEl);
return fullImage.play(); file.audio = audioEl;
} else { }
return post.file.wasPlaying = true; }
} },
}); completeExpand(post) {
if (controls) { const { file } = post;
return ImageCommon.addControls(fullImage); if (!file.isExpanding) {
} return;
}, } // contracted before the image loaded
const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height;
videoCB: (function() { const oldHeight = d$1.body.clientHeight;
// dragging to the left contracts the video const { scrollY } = window;
let mousedown = false; $$1.addClass(post.nodes.root, 'expanded-image');
return { $$1.rmClass(file.thumb, 'expanding');
mouseover() { return mousedown = false; }, file.isExpanded = true;
mousedown(e) { if (e.button === 0) { return mousedown = true; } }, delete file.isExpanding;
mouseup(e) { if (e.button === 0) { return mousedown = false; } }, // Scroll to keep our place in the thread when images are expanded above us.
mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) { return ImageExpand.toggle(Get$1.postFromNode(this)); } } if (doc$1.contains(post.nodes.root) && (bottom <= 0)) {
}; window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
})(), }
// Scroll to display full image.
setupVideoCB(post) { if (file.scrollIntoView) {
for (var eventName in ImageExpand.videoCB) { delete file.scrollIntoView;
var cb = ImageExpand.videoCB[eventName]; const imageBottom = Math.min(doc$1.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header$1.getBottomOf(file.fullImage));
$$1.on(post.file.fullImage, eventName, cb); if (imageBottom < 0) {
} window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage)));
if (post.file.videoControls) { }
return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post)); }
} if (file.isVideo) {
}, return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']);
}
error() { },
const post = Get$1.postFromNode(this); setupVideo(post, playing, controls) {
$$1.rm(this); const { fullImage } = post.file;
delete post.file.fullImage; if (!playing) {
// Images can error: fullImage.controls = controls;
// - before the image started loading. return;
// - after the image started loading. }
// Don't try to re-expand if it was already contracted. fullImage.controls = false;
if (!post.file.isExpanding && !post.file.isExpanded) { return; } $$1.asap((() => doc$1.contains(fullImage)), function () {
if (ImageCommon.decodeError(this, post.file)) { if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) {
return ImageExpand.contract(post); return fullImage.play();
} }
// Don't autoretry images from the archive. else {
if (ImageCommon.isFromArchive(this)) { return post.file.wasPlaying = true;
return ImageExpand.contract(post); }
} });
return ImageCommon.error(this, post, post.file, 10 * SECOND, function(URL) { if (controls) {
if (post.file.isExpanding || post.file.isExpanded) { return ImageCommon.addControls(fullImage);
ImageExpand.contract(post); }
if (URL) { return ImageExpand.expand(post, URL); } },
} videoCB: (function () {
}); // dragging to the left contracts the video
}, let mousedown = false;
return {
menu: { mouseover() { return mousedown = false; },
init() { mousedown(e) { if (e.button === 0) {
if (!ImageExpand.enabled) { return; } return mousedown = true;
} },
const el = $$1.el('span', { mouseup(e) { if (e.button === 0) {
textContent: 'Image Expansion', return mousedown = false;
className: 'image-expansion-link' } },
} mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) {
); return ImageExpand.toggle(Get$1.postFromNode(this));
} }
const {createSubEntry} = ImageExpand.menu; };
const subEntries = []; })(),
for (var name in Config.imageExpansion) { setupVideoCB(post) {
var conf = Config.imageExpansion[name]; for (var eventName in ImageExpand.videoCB) {
subEntries.push(createSubEntry(name, conf[1])); var cb = ImageExpand.videoCB[eventName];
} $$1.on(post.file.fullImage, eventName, cb);
}
return Header$1.menu.addEntry({ if (post.file.videoControls) {
el, return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post));
order: 105, }
subEntries },
}); error() {
}, const post = Get$1.postFromNode(this);
$$1.rm(this);
createSubEntry(name, desc) { delete post.file.fullImage;
const label = UI.checkbox(name, name); // Images can error:
label.title = desc; // - before the image started loading.
const input = label.firstElementChild; // - after the image started loading.
if (['Fit width', 'Fit height'].includes(name)) { // Don't try to re-expand if it was already contracted.
$$1.on(input, 'change', ImageExpand.cb.setFitness); if (!post.file.isExpanding && !post.file.isExpanded) {
} return;
$$1.event('change', null, input); }
$$1.on(input, 'change', $$1.cb.checked); if (ImageCommon.decodeError(this, post.file)) {
return {el: label}; return ImageExpand.contract(post);
} }
} // Don't autoretry images from the archive.
if (ImageCommon.isFromArchive(this)) {
return ImageExpand.contract(post);
}
return ImageCommon.error(this, post, post.file, 10 * SECOND, function (URL) {
if (post.file.isExpanding || post.file.isExpanded) {
ImageExpand.contract(post);
if (URL) {
return ImageExpand.expand(post, URL);
}
}
});
},
menu: {
init() {
if (!ImageExpand.enabled) {
return;
}
const el = $$1.el('span', {
textContent: 'Image Expansion',
className: 'image-expansion-link'
});
const { createSubEntry } = ImageExpand.menu;
const subEntries = [];
for (var name in Config.imageExpansion) {
var conf = Config.imageExpansion[name];
subEntries.push(createSubEntry(name, conf[1]));
}
return Header$1.menu.addEntry({
el,
order: 105,
subEntries
});
},
createSubEntry(name, desc) {
const label = UI.checkbox(name, name);
label.title = desc;
const input = label.firstElementChild;
if (['Fit width', 'Fit height'].includes(name)) {
$$1.on(input, 'change', ImageExpand.cb.setFitness);
}
$$1.event('change', null, input);
$$1.on(input, 'change', $$1.cb.checked);
return { el: label };
}
}
}; };
class Post { class Post {
@ -11065,6 +11154,10 @@ textarea.copy-text-element {
} }
/* File */ /* File */
.expanded-image > .post > .file > .fileThumb {
display: flex;
flex-direction: column;
}
.fileText-original, .fileText-original,
.fnswitch:hover > .fntrunc, .fnswitch:hover > .fntrunc,
.fnswitch:not(:hover) > .fnfull, .fnswitch:not(:hover) > .fnfull,
@ -11079,6 +11172,11 @@ textarea.copy-text-element {
.expanded-image > .post > .file > .fileThumb > .full-image { .expanded-image > .post > .file > .fileThumb > .full-image {
display: inline; display: inline;
} }
.expanded-image > .post > .file > .fileThumb > audio {
height: 30px;
width: 100%;
min-width: 300px;
}
.expanded-image { .expanded-image {
clear: left; clear: left;
} }
@ -21341,7 +21439,7 @@ vp-replace
} else { } else {
$.open = $.open =
url => window.open(url, '_blank'); url => window.open(url, '_blank');
} }
$.debounce = function(wait, fn) { $.debounce = function(wait, fn) {
let lastCall = 0; let lastCall = 0;
@ -21432,7 +21530,9 @@ vp-replace
: :
value; value;
$.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount; $.hasAudio = video =>
video.mozHasAudio || !!video.webkitAudioDecodedByteCount ||
video.nextElementSibling?.tagName === 'AUDIO'; // sound posts
$.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114); $.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -614,7 +614,11 @@ div.boardTitle {
'Volume in New Tab': [ 'Volume in New Tab': [
true, true,
`Apply ${meta.name} mute and volume settings to videos opened in their own tabs.` `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.`
] ],
'Enable sound posts': [
true,
'Enable loading audio from [sound=] file names. This audio is fetched from third parties.'
],
}, },
'Menu': { 'Menu': {
@ -3267,391 +3271,476 @@ https://*.hcaptcha.com
} }
}; };
const Audio = {
/** Add event listeners for videos with audio from a third party */
setupSync(video, audio) {
video.addEventListener('playing', () => {
audio.currentTime = video.currentTime;
audio.play();
});
video.addEventListener('pause', () => {
audio.pause();
});
video.addEventListener('seeked', () => {
audio.currentTime = video.currentTime;
});
video.addEventListener('ratechange', () => {
audio.currentTime = video.currentTime;
audio.playbackRate = video.playbackRate;
});
video.addEventListener('waiting', () => {
audio.currentTime = video.currentTime;
audio.pause();
});
audio.addEventListener('canplay', () => {
if (audio.currentTime < .1)
video.currentTime = 0;
}, { once: true });
},
setupAudioSlider(video, audio) {
const container = document.createElement('span');
// \u00A0 is non breaking space
container.appendChild(document.createTextNode('🔊︎\u00A0'));
const control = document.createElement('input');
control.type = 'range';
control.max = '1';
control.step = '0.01';
control.valueAsNumber = audio.volume;
control.addEventListener('input', () => {
audio.volume = control.valueAsNumber;
});
container.appendChild(control);
const downloadLink = document.createElement('a');
downloadLink.href = audio.src;
downloadLink.download = '';
downloadLink.target = '_blank';
downloadLink.textContent = '\u00A0📥';
container.appendChild(downloadLink);
return container;
},
};
/* /*
* decaffeinate suggestions: * decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns * DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/ */
var ImageExpand = { var ImageExpand = {
init() {
if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { return; }
this.EAI = $$1.el('a', {
className: 'expand-all-shortcut',
textContent: '',
title: 'Expand All Images',
href: 'javascript:;'
}
);
$$1.on(this.EAI, 'click', this.cb.toggleAll);
Header$1.addShortcut('expand-all', this.EAI, 520);
$$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos);
this.videoControls = $$1.el('span', {className: 'video-controls'});
$$1.extend(this.videoControls, {innerHTML: " <a href=\"javascript:;\" title=\"You can also contract the video by dragging it to the left.\">contract</a>"});
return Callbacks.Post.push({
name: 'Image Expansion',
cb: this.node
});
},
node() {
if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; }
$$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
if (this.isClone) {
if (this.file.isExpanding) {
// If we clone a post where the image is still loading,
// make it loading in the clone too.
ImageExpand.contract(this);
return ImageExpand.expand(this);
} else if (this.file.isExpanded && this.file.isVideo) {
Volume.setup(this.file.fullImage);
ImageExpand.setupVideoCB(this);
return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls);
}
} else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote &&
(Conf['Expand spoilers'] || !this.file.isSpoiler) &&
(Conf['Expand videos'] || !this.file.isVideo)) {
return ImageExpand.expand(this);
}
},
cb: {
toggle(e) {
if ($$1.modifiedClick(e)) { return; }
const post = Get$1.postFromNode(this);
const {file} = post;
if (file.isExpanded && ImageCommon.onControls(e)) { return; }
e.preventDefault();
if (!Conf['Autoplay'] && file.fullImage?.paused) {
return file.fullImage.play();
} else {
return ImageExpand.toggle(post);
}
},
toggleAll() {
let func;
$$1.event('CloseMenu');
const threadRoot = Nav.getThread();
const toggle = function(post) {
const {file} = post;
if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) { return; }
if (ImageExpand.on &&
((!Conf['Expand spoilers'] && file.isSpoiler) ||
(!Conf['Expand videos'] && file.isVideo) ||
(Conf['Expand from here'] && (Header$1.getTopOf(file.thumb) < 0)) ||
(Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) {
return;
}
return $$1.queueTask(func, post);
};
if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) {
ImageExpand.EAI.className = 'contract-all-shortcut';
ImageExpand.EAI.title = 'Contract All Images';
ImageExpand.EAI.textContent = '';
func = ImageExpand.expand;
} else {
ImageExpand.EAI.className = 'expand-all-shortcut';
ImageExpand.EAI.title = 'Expand All Images';
ImageExpand.EAI.textContent = '';
func = ImageExpand.contract;
}
return g.posts.forEach(function(post) {
for (post of [post, ...post.clones]) { toggle(post); }
});
},
playVideos() {
return g.posts.forEach(function(post) {
for (post of [post, ...post.clones]) {
var {file} = post;
if (!file || !file.isVideo || !file.isExpanded) { continue; }
var video = file.fullImage;
var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video);
if (visible && file.wasPlaying) {
delete file.wasPlaying;
video.play();
} else if (!visible && !video.paused) {
file.wasPlaying = true;
video.pause();
}
}
});
},
setFitness() {
return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-'));
}
},
toggle(post) {
if (!post.file.isExpanding && !post.file.isExpanded) {
post.file.scrollIntoView = Conf['Scroll into view'];
ImageExpand.expand(post);
return;
}
ImageExpand.contract(post);
if (Conf['Advance on contract']) {
let next = post.nodes.root;
while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) {
if (!$$1('.stub', next) && (next.offsetHeight !== 0)) { break; }
}
if (next) {
return Header$1.scrollTo(next);
}
}
},
contract(post) {
let bottom, el, oldHeight, scrollY;
const {file} = post;
if (el = file.fullImage) {
const top = Header$1.getTopOf(el);
bottom = top + el.getBoundingClientRect().height;
oldHeight = d$1.body.clientHeight;
({scrollY} = window);
}
$$1.rmClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding');
$$1.rm(file.videoControls);
file.thumbLink.href = file.url;
file.thumbLink.target = '_blank';
for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) {
delete file[x];
}
if (!el) { return; }
if (doc$1.contains(el)) {
if (bottom <= 0) {
// For images entirely above us, scroll to remain in place.
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
} else {
// For images not above us that would be moved above us, scroll to the thumbnail.
Header$1.scrollToIfNeeded(post.nodes.root);
}
if (window.scrollX > 0) {
// If we have scrolled right viewing an expanded image, return to the left.
window.scrollBy(-window.scrollX, 0);
}
}
$$1.off(el, 'error', ImageExpand.error);
ImageCommon.pushCache(el);
if (file.isVideo) {
ImageCommon.pause(el);
for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName];
$$1.off(el, eventName, cb);
}
}
if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); }
delete file.fullImage;
return $$1.queueTask(function() {
// XXX Work around Chrome/Chromium not firing mouseover on the thumbnail.
if (file.isExpanding || file.isExpanded) { return; }
$$1.rmClass(el, 'full-image');
if (el.id) { return; }
return $$1.rm(el);
});
},
expand(post, src) {
// Do not expand images of hidden/filtered replies, or already expanded pictures.
let el;
const {file} = post;
const {thumb, thumbLink, isVideo} = file;
if (post.isHidden || file.isExpanding || file.isExpanded) { return; }
$$1.addClass(thumb, 'expanding');
file.isExpanding = true;
if (file.fullImage) {
el = file.fullImage;
} else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) {
el = (file.fullImage = ImageCommon.popCache());
$$1.on(el, 'error', ImageExpand.error);
if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { ImageCommon.rewind(el); }
el.removeAttribute('id');
} else {
el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img')));
el.dataset.fileID = `${post.fullID}.${file.index}`;
$$1.on(el, 'error', ImageExpand.error);
el.src = src || file.url;
}
el.className = 'full-image';
$$1.after(thumb, el);
if (isVideo) {
// add contract link to file info
if (!file.videoControls) {
file.videoControls = ImageExpand.videoControls.cloneNode(true);
$$1.add(file.text, file.videoControls);
}
// disable link to file so native controls can work
thumbLink.removeAttribute('href');
thumbLink.removeAttribute('target');
el.loop = true;
Volume.setup(el);
ImageExpand.setupVideoCB(post);
}
if (!isVideo) {
return $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
} else if (el.readyState >= el.HAVE_METADATA) {
return ImageExpand.completeExpand(post);
} else {
return $$1.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post));
}
},
completeExpand(post) {
const {file} = post;
if (!file.isExpanding) { return; } // contracted before the image loaded
const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height;
const oldHeight = d$1.body.clientHeight;
const {scrollY} = window;
$$1.addClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding');
file.isExpanded = true;
delete file.isExpanding;
// Scroll to keep our place in the thread when images are expanded above us.
if (doc$1.contains(post.nodes.root) && (bottom <= 0)) {
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
}
// Scroll to display full image.
if (file.scrollIntoView) {
delete file.scrollIntoView;
const imageBottom = Math.min(doc$1.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header$1.getBottomOf(file.fullImage));
if (imageBottom < 0) {
window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage)));
}
}
if (file.isVideo) {
return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']);
}
},
setupVideo(post, playing, controls) {
const {fullImage} = post.file;
if (!playing) {
fullImage.controls = controls;
return;
}
fullImage.controls = false;
$$1.asap((() => doc$1.contains(fullImage)), function() {
if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) {
return fullImage.play();
} else {
return post.file.wasPlaying = true;
}
});
if (controls) {
return ImageCommon.addControls(fullImage);
}
},
videoCB: (function() {
// dragging to the left contracts the video
let mousedown = false;
return {
mouseover() { return mousedown = false; },
mousedown(e) { if (e.button === 0) { return mousedown = true; } },
mouseup(e) { if (e.button === 0) { return mousedown = false; } },
mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) { return ImageExpand.toggle(Get$1.postFromNode(this)); } }
};
})(),
setupVideoCB(post) {
for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName];
$$1.on(post.file.fullImage, eventName, cb);
}
if (post.file.videoControls) {
return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post));
}
},
error() {
const post = Get$1.postFromNode(this);
$$1.rm(this);
delete post.file.fullImage;
// Images can error:
// - before the image started loading.
// - after the image started loading.
// Don't try to re-expand if it was already contracted.
if (!post.file.isExpanding && !post.file.isExpanded) { return; }
if (ImageCommon.decodeError(this, post.file)) {
return ImageExpand.contract(post);
}
// Don't autoretry images from the archive.
if (ImageCommon.isFromArchive(this)) {
return ImageExpand.contract(post);
}
return ImageCommon.error(this, post, post.file, 10 * SECOND, function(URL) {
if (post.file.isExpanding || post.file.isExpanded) {
ImageExpand.contract(post);
if (URL) { return ImageExpand.expand(post, URL); }
}
});
},
menu: {
init() { init() {
if (!ImageExpand.enabled) { return; } if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) {
return;
const el = $$1.el('span', { }
textContent: 'Image Expansion', this.EAI = $$1.el('a', {
className: 'image-expansion-link' className: 'expand-all-shortcut',
} textContent: '',
); title: 'Expand All Images',
href: 'javascript:;'
const {createSubEntry} = ImageExpand.menu; });
const subEntries = []; $$1.on(this.EAI, 'click', this.cb.toggleAll);
for (var name in Config.imageExpansion) { Header$1.addShortcut('expand-all', this.EAI, 520);
var conf = Config.imageExpansion[name]; $$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos);
subEntries.push(createSubEntry(name, conf[1])); this.videoControls = $$1.el('span', { className: 'video-controls' });
} $$1.extend(this.videoControls, { innerHTML: " <a href=\"javascript:;\" title=\"You can also contract the video by dragging it to the left.\">contract</a>" });
return Callbacks.Post.push({
return Header$1.menu.addEntry({ name: 'Image Expansion',
el, cb: this.node
order: 105, });
subEntries
});
}, },
node() {
createSubEntry(name, desc) { if (!this.file || (!this.file.isImage && !this.file.isVideo)) {
const label = UI.checkbox(name, name); return;
label.title = desc; }
const input = label.firstElementChild; $$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
if (['Fit width', 'Fit height'].includes(name)) { if (this.isClone) {
$$1.on(input, 'change', ImageExpand.cb.setFitness); if (this.file.isExpanding) {
} // If we clone a post where the image is still loading,
$$1.event('change', null, input); // make it loading in the clone too.
$$1.on(input, 'change', $$1.cb.checked); ImageExpand.contract(this);
return {el: label}; return ImageExpand.expand(this);
}
else if (this.file.isExpanded && this.file.isVideo) {
Volume.setup(this.file.fullImage);
ImageExpand.setupVideoCB(this);
return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls);
}
}
else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote &&
(Conf['Expand spoilers'] || !this.file.isSpoiler) &&
(Conf['Expand videos'] || !this.file.isVideo)) {
return ImageExpand.expand(this);
}
},
cb: {
toggle(e) {
if ($$1.modifiedClick(e)) {
return;
}
const post = Get$1.postFromNode(this);
const { file } = post;
if (file.isExpanded && ImageCommon.onControls(e)) {
return;
}
e.preventDefault();
if (!Conf['Autoplay'] && file.fullImage?.paused) {
return file.fullImage.play();
}
else {
return ImageExpand.toggle(post);
}
},
toggleAll() {
let func;
$$1.event('CloseMenu');
const threadRoot = Nav.getThread();
const toggle = function (post) {
const { file } = post;
if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) {
return;
}
if (ImageExpand.on &&
((!Conf['Expand spoilers'] && file.isSpoiler) ||
(!Conf['Expand videos'] && file.isVideo) ||
(Conf['Expand from here'] && (Header$1.getTopOf(file.thumb) < 0)) ||
(Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) {
return;
}
return $$1.queueTask(func, post);
};
if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) {
ImageExpand.EAI.className = 'contract-all-shortcut';
ImageExpand.EAI.title = 'Contract All Images';
ImageExpand.EAI.textContent = '';
func = ImageExpand.expand;
}
else {
ImageExpand.EAI.className = 'expand-all-shortcut';
ImageExpand.EAI.title = 'Expand All Images';
ImageExpand.EAI.textContent = '';
func = ImageExpand.contract;
}
return g.posts.forEach(function (post) {
for (post of [post, ...post.clones]) {
toggle(post);
}
});
},
playVideos() {
return g.posts.forEach(function (post) {
for (post of [post, ...post.clones]) {
var { file } = post;
if (!file || !file.isVideo || !file.isExpanded) {
continue;
}
var video = file.fullImage;
var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video);
if (visible && file.wasPlaying) {
delete file.wasPlaying;
video.play();
}
else if (!visible && !video.paused) {
file.wasPlaying = true;
video.pause();
}
}
});
},
setFitness() {
return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-'));
}
},
toggle(post) {
if (!post.file.isExpanding && !post.file.isExpanded) {
post.file.scrollIntoView = Conf['Scroll into view'];
ImageExpand.expand(post);
return;
}
ImageExpand.contract(post);
if (Conf['Advance on contract']) {
let next = post.nodes.root;
while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) {
if (!$$1('.stub', next) && (next.offsetHeight !== 0)) {
break;
}
}
if (next) {
return Header$1.scrollTo(next);
}
}
},
contract(post) {
let bottom, el, oldHeight, scrollY;
const { file } = post;
if (el = file.fullImage) {
const top = Header$1.getTopOf(el);
bottom = top + el.getBoundingClientRect().height;
oldHeight = d$1.body.clientHeight;
({ scrollY } = window);
}
$$1.rmClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding');
$$1.rm(file.videoControls);
file.thumbLink.href = file.url;
file.thumbLink.target = '_blank';
for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) {
delete file[x];
}
if (!el) {
return;
}
if (doc$1.contains(el)) {
if (bottom <= 0) {
// For images entirely above us, scroll to remain in place.
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
}
else {
// For images not above us that would be moved above us, scroll to the thumbnail.
Header$1.scrollToIfNeeded(post.nodes.root);
}
if (window.scrollX > 0) {
// If we have scrolled right viewing an expanded image, return to the left.
window.scrollBy(-window.scrollX, 0);
}
}
$$1.off(el, 'error', ImageExpand.error);
ImageCommon.pushCache(el);
if (file.isVideo) {
ImageCommon.pause(el);
for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName];
$$1.off(el, eventName, cb);
}
}
if (Conf['Restart when Opened']) {
ImageCommon.rewind(file.thumb);
}
delete file.fullImage;
$$1.queueTask(function () {
// XXX Work around Chrome/Chromium not firing mouseover on the thumbnail.
if (file.isExpanding || file.isExpanded) {
return;
}
$$1.rmClass(el, 'full-image');
if (el.id) {
return;
}
return $$1.rm(el);
});
if (file.audio) {
file.audio.remove();
delete file.audio;
if (file.audioSlider) {
file.audioSlider.remove();
delete file.audioSlider;
}
}
},
expand(post, src) {
const { file } = post;
const { thumb, thumbLink, isVideo } = file;
// Do not expand images of hidden/filtered replies, or already expanded pictures.
if (post.isHidden || file.isExpanding || file.isExpanded) {
return;
}
let el;
$$1.addClass(thumb, 'expanding');
file.isExpanding = true;
if (file.fullImage) {
el = file.fullImage;
}
else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) {
el = (file.fullImage = ImageCommon.popCache());
$$1.on(el, 'error', ImageExpand.error);
if (Conf['Restart when Opened'] && (el.id !== 'ihover')) {
ImageCommon.rewind(el);
}
el.removeAttribute('id');
}
else {
el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img')));
el.dataset.fileID = `${post.fullID}.${file.index}`;
$$1.on(el, 'error', ImageExpand.error);
el.src = src || file.url;
}
el.className = 'full-image';
$$1.after(thumb, el);
if (isVideo) {
// add contract link to file info
if (!file.videoControls) {
file.videoControls = ImageExpand.videoControls.cloneNode(true);
$$1.add(file.text, file.videoControls);
}
// disable link to file so native controls can work
thumbLink.removeAttribute('href');
thumbLink.removeAttribute('target');
el.loop = true;
Volume.setup(el);
ImageExpand.setupVideoCB(post);
}
if (!isVideo) {
$$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
}
else if (el.readyState >= el.HAVE_METADATA) {
ImageExpand.completeExpand(post);
}
else {
$$1.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post));
}
if (Conf['Enable sound posts'] && Conf['Allow Sound']) {
const soundUrlMatch = file.name.match(/\[sound=([^\]]+)]/);
if (soundUrlMatch) {
let src = decodeURIComponent(soundUrlMatch[1]);
if (!src.startsWith('http'))
src = `https://${src}`;
const audioEl = $$1.el('audio', { src });
Volume.setup(audioEl);
if (isVideo) {
Audio.setupSync(el, audioEl);
if (Conf['Show Controls']) {
file.audioSlider = Audio.setupAudioSlider(el, audioEl);
$$1.after(el.parentElement, file.audioSlider);
}
}
else {
audioEl.controls = Conf['Show Controls'];
audioEl.autoplay = Conf['Autoplay'];
}
$$1.after(el, audioEl);
file.audio = audioEl;
}
}
},
completeExpand(post) {
const { file } = post;
if (!file.isExpanding) {
return;
} // contracted before the image loaded
const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height;
const oldHeight = d$1.body.clientHeight;
const { scrollY } = window;
$$1.addClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding');
file.isExpanded = true;
delete file.isExpanding;
// Scroll to keep our place in the thread when images are expanded above us.
if (doc$1.contains(post.nodes.root) && (bottom <= 0)) {
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
}
// Scroll to display full image.
if (file.scrollIntoView) {
delete file.scrollIntoView;
const imageBottom = Math.min(doc$1.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header$1.getBottomOf(file.fullImage));
if (imageBottom < 0) {
window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage)));
}
}
if (file.isVideo) {
return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']);
}
},
setupVideo(post, playing, controls) {
const { fullImage } = post.file;
if (!playing) {
fullImage.controls = controls;
return;
}
fullImage.controls = false;
$$1.asap((() => doc$1.contains(fullImage)), function () {
if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) {
return fullImage.play();
}
else {
return post.file.wasPlaying = true;
}
});
if (controls) {
return ImageCommon.addControls(fullImage);
}
},
videoCB: (function () {
// dragging to the left contracts the video
let mousedown = false;
return {
mouseover() { return mousedown = false; },
mousedown(e) { if (e.button === 0) {
return mousedown = true;
} },
mouseup(e) { if (e.button === 0) {
return mousedown = false;
} },
mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) {
return ImageExpand.toggle(Get$1.postFromNode(this));
} }
};
})(),
setupVideoCB(post) {
for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName];
$$1.on(post.file.fullImage, eventName, cb);
}
if (post.file.videoControls) {
return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post));
}
},
error() {
const post = Get$1.postFromNode(this);
$$1.rm(this);
delete post.file.fullImage;
// Images can error:
// - before the image started loading.
// - after the image started loading.
// Don't try to re-expand if it was already contracted.
if (!post.file.isExpanding && !post.file.isExpanded) {
return;
}
if (ImageCommon.decodeError(this, post.file)) {
return ImageExpand.contract(post);
}
// Don't autoretry images from the archive.
if (ImageCommon.isFromArchive(this)) {
return ImageExpand.contract(post);
}
return ImageCommon.error(this, post, post.file, 10 * SECOND, function (URL) {
if (post.file.isExpanding || post.file.isExpanded) {
ImageExpand.contract(post);
if (URL) {
return ImageExpand.expand(post, URL);
}
}
});
},
menu: {
init() {
if (!ImageExpand.enabled) {
return;
}
const el = $$1.el('span', {
textContent: 'Image Expansion',
className: 'image-expansion-link'
});
const { createSubEntry } = ImageExpand.menu;
const subEntries = [];
for (var name in Config.imageExpansion) {
var conf = Config.imageExpansion[name];
subEntries.push(createSubEntry(name, conf[1]));
}
return Header$1.menu.addEntry({
el,
order: 105,
subEntries
});
},
createSubEntry(name, desc) {
const label = UI.checkbox(name, name);
label.title = desc;
const input = label.firstElementChild;
if (['Fit width', 'Fit height'].includes(name)) {
$$1.on(input, 'change', ImageExpand.cb.setFitness);
}
$$1.event('change', null, input);
$$1.on(input, 'change', $$1.cb.checked);
return { el: label };
}
} }
}
}; };
class Post { class Post {
@ -10954,6 +11043,10 @@ textarea.copy-text-element {
} }
/* File */ /* File */
.expanded-image > .post > .file > .fileThumb {
display: flex;
flex-direction: column;
}
.fileText-original, .fileText-original,
.fnswitch:hover > .fntrunc, .fnswitch:hover > .fntrunc,
.fnswitch:not(:hover) > .fnfull, .fnswitch:not(:hover) > .fnfull,
@ -10968,6 +11061,11 @@ textarea.copy-text-element {
.expanded-image > .post > .file > .fileThumb > .full-image { .expanded-image > .post > .file > .fileThumb > .full-image {
display: inline; display: inline;
} }
.expanded-image > .post > .file > .fileThumb > audio {
height: 30px;
width: 100%;
min-width: 300px;
}
.expanded-image { .expanded-image {
clear: left; clear: left;
} }
@ -21230,7 +21328,7 @@ vp-replace
} else { } else {
$.open = $.open =
url => window.open(url, '_blank'); url => window.open(url, '_blank');
} }
$.debounce = function(wait, fn) { $.debounce = function(wait, fn) {
let lastCall = 0; let lastCall = 0;
@ -21321,7 +21419,9 @@ vp-replace
: :
value; value;
$.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount; $.hasAudio = video =>
video.mozHasAudio || !!video.webkitAudioDecodedByteCount ||
video.nextElementSibling?.tagName === 'AUDIO'; // sound posts
$.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114); $.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114);

58
src/Images/Audio.ts Normal file
View File

@ -0,0 +1,58 @@
const Audio = {
/** Add event listeners for videos with audio from a third party */
setupSync(video: HTMLVideoElement, audio: HTMLAudioElement) {
video.addEventListener('playing', () => {
audio.currentTime = video.currentTime;
audio.play();
});
video.addEventListener('pause', () => {
audio.pause();
});
video.addEventListener('seeked', () => {
audio.currentTime = video.currentTime;
});
video.addEventListener('ratechange', () => {
audio.currentTime = video.currentTime;
audio.playbackRate = video.playbackRate;
});
video.addEventListener('waiting', () => {
audio.currentTime = video.currentTime;
audio.pause();
});
audio.addEventListener('canplay', () => {
if (audio.currentTime < .1) video.currentTime = 0;
}, { once: true });
},
setupAudioSlider(video: HTMLVideoElement, audio: HTMLAudioElement): HTMLSpanElement {
const container = document.createElement('span');
// \u00A0 is non breaking space
container.appendChild(document.createTextNode('🔊︎\u00A0'));
const control = document.createElement('input');
control.type = 'range';
control.max = '1';
control.step = '0.01';
control.valueAsNumber = audio.volume;
control.addEventListener('input', () => {
audio.volume = control.valueAsNumber;
});
container.appendChild(control);
const downloadLink = document.createElement('a');
downloadLink.href = audio.src;
downloadLink.download = '';
downloadLink.target = '_blank';
downloadLink.textContent = '\u00A0📥';
container.appendChild(downloadLink)
return container;
},
};
export default Audio;

View File

@ -9,6 +9,8 @@ import $ from "../platform/$";
import { SECOND } from "../platform/helpers"; import { SECOND } from "../platform/helpers";
import ImageCommon from "./ImageCommon"; import ImageCommon from "./ImageCommon";
import Volume from "./Volume"; import Volume from "./Volume";
import Audio from "./Audio";
import type { default as Post, PostClone } from "../classes/Post";
/* /*
* decaffeinate suggestions: * decaffeinate suggestions:
@ -39,7 +41,7 @@ var ImageExpand = {
}); });
}, },
node() { node(this: Post | PostClone) {
if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; } if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; }
$.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle); $.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
@ -202,22 +204,32 @@ var ImageExpand = {
} }
if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); } if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); }
delete file.fullImage; delete file.fullImage;
return $.queueTask(function() { $.queueTask(function() {
// XXX Work around Chrome/Chromium not firing mouseover on the thumbnail. // XXX Work around Chrome/Chromium not firing mouseover on the thumbnail.
if (file.isExpanding || file.isExpanded) { return; } if (file.isExpanding || file.isExpanded) { return; }
$.rmClass(el, 'full-image'); $.rmClass(el, 'full-image');
if (el.id) { return; } if (el.id) { return; }
return $.rm(el); return $.rm(el);
}); });
if (file.audio) {
file.audio.remove();
delete file.audio;
if (file.audioSlider) {
file.audioSlider.remove();
delete file.audioSlider;
}
}
}, },
expand(post, src) { expand(post: Post, src?: string) {
// Do not expand images of hidden/filtered replies, or already expanded pictures.
let el;
const {file} = post; const {file} = post;
const {thumb, thumbLink, isVideo} = file; const {thumb, thumbLink, isVideo } = file;
// Do not expand images of hidden/filtered replies, or already expanded pictures.
if (post.isHidden || file.isExpanding || file.isExpanded) { return; } if (post.isHidden || file.isExpanding || file.isExpanded) { return; }
let el: HTMLImageElement| HTMLVideoElement;
$.addClass(thumb, 'expanding'); $.addClass(thumb, 'expanding');
file.isExpanding = true; file.isExpanding = true;
@ -255,11 +267,33 @@ var ImageExpand = {
} }
if (!isVideo) { if (!isVideo) {
return $.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post)); $.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
} else if (el.readyState >= el.HAVE_METADATA) { } else if (el.readyState >= el.HAVE_METADATA) {
return ImageExpand.completeExpand(post); ImageExpand.completeExpand(post);
} else { } else {
return $.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post)); $.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post));
}
if (Conf['Enable sound posts'] && Conf['Allow Sound']) {
const soundUrlMatch = file.name.match(/\[sound=([^\]]+)]/);
if (soundUrlMatch) {
let src = decodeURIComponent(soundUrlMatch[1]);
if (!src.startsWith('http')) src = `https://${src}`;
const audioEl: HTMLAudioElement = $.el('audio', { src });
Volume.setup(audioEl);
if (isVideo) {
Audio.setupSync(el as HTMLVideoElement, audioEl);
if (Conf['Show Controls']) {
file.audioSlider = Audio.setupAudioSlider(el as HTMLVideoElement, audioEl);
$.after(el.parentElement, file.audioSlider);
}
} else {
audioEl.controls = Conf['Show Controls'];
audioEl.autoplay = Conf['Autoplay'];
}
$.after(el, audioEl);
file.audio = audioEl;
}
} }
}, },
@ -295,7 +329,7 @@ var ImageExpand = {
} }
}, },
setupVideo(post, playing, controls) { setupVideo(post: Post, playing: boolean, controls: boolean) {
const {fullImage} = post.file; const {fullImage} = post.file;
if (!playing) { if (!playing) {
fullImage.controls = controls; fullImage.controls = controls;

View File

@ -7,6 +7,25 @@ import type Board from "./Board";
import Callbacks from "./Callbacks"; import Callbacks from "./Callbacks";
import type Thread from "./Thread"; import type Thread from "./Thread";
export interface File {
text: string,
link: HTMLAnchorElement,
thumb: HTMLElement,
thumbLink: HTMLElement,
size: string,
sizeInBytes: number,
isDead: boolean,
url: string,
name: string,
isImage: boolean,
isVideo: boolean,
isExpanding: boolean,
isExpanded: boolean,
fullImage?: HTMLImageElement | HTMLVideoElement,
audio?: HTMLAudioElement,
audioSlider?:HTMLSpanElement,
};
export default class Post { export default class Post {
declare root: HTMLElement; declare root: HTMLElement;
declare thread: Thread; declare thread: Thread;
@ -291,15 +310,7 @@ export default class Post {
} }
parseFile(fileRoot: HTMLElement) { parseFile(fileRoot: HTMLElement) {
interface File {
text: string,
link: HTMLAnchorElement,
thumb: HTMLElement,
thumbLink: HTMLElement,
size: string,
sizeInBytes: number,
isDead: boolean,
};
const file: Partial<File> = { isDead: false }; const file: Partial<File> = { isDead: false };
for (var key in g.SITE.selectors.file) { for (var key in g.SITE.selectors.file) {

View File

@ -324,7 +324,11 @@ const Config = {
'Volume in New Tab': [ 'Volume in New Tab': [
true, true,
`Apply ${meta.name} mute and volume settings to videos opened in their own tabs.` `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.`
] ],
'Enable sound posts': [
true,
'Enable loading audio from [sound=] file names. This audio is fetched from third parties.'
],
}, },
'Menu': { 'Menu': {

View File

@ -1341,6 +1341,10 @@ textarea.copy-text-element {
} }
/* File */ /* File */
.expanded-image > .post > .file > .fileThumb {
display: flex;
flex-direction: column;
}
.fileText-original, .fileText-original,
.fnswitch:hover > .fntrunc, .fnswitch:hover > .fntrunc,
.fnswitch:not(:hover) > .fnfull, .fnswitch:not(:hover) > .fnfull,
@ -1355,6 +1359,11 @@ textarea.copy-text-element {
.expanded-image > .post > .file > .fileThumb > .full-image { .expanded-image > .post > .file > .fileThumb > .full-image {
display: inline; display: inline;
} }
.expanded-image > .post > .file > .fileThumb > audio {
height: 30px;
width: 100%;
min-width: 300px;
}
.expanded-image { .expanded-image {
clear: left; clear: left;
} }

View File

@ -465,7 +465,7 @@ if (!globalThis.chrome?.extension) {
} else { } else {
$.open = $.open =
url => window.open(url, '_blank'); url => window.open(url, '_blank');
} }
$.debounce = function(wait, fn) { $.debounce = function(wait, fn) {
let lastCall = 0; let lastCall = 0;
@ -556,7 +556,9 @@ $.minmax = (value, min, max) => value < min ?
: :
value; value;
$.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount; $.hasAudio = video =>
video.mozHasAudio || !!video.webkitAudioDecodedByteCount ||
video.nextElementSibling?.tagName === 'AUDIO'; // sound posts
$.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114); $.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114);