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': [
true,
`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': {
@ -3378,391 +3382,476 @@ https://*.hcaptcha.com
}
};
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
var ImageExpand = {
init() {
if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { return; }
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;
},
};
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() {
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};
}
}
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
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;
$$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 {
@ -11065,6 +11154,10 @@ textarea.copy-text-element {
}
/* File */
.expanded-image > .post > .file > .fileThumb {
display: flex;
flex-direction: column;
}
.fileText-original,
.fnswitch:hover > .fntrunc,
.fnswitch:not(:hover) > .fnfull,
@ -11079,6 +11172,11 @@ textarea.copy-text-element {
.expanded-image > .post > .file > .fileThumb > .full-image {
display: inline;
}
.expanded-image > .post > .file > .fileThumb > audio {
height: 30px;
width: 100%;
min-width: 300px;
}
.expanded-image {
clear: left;
}
@ -21341,7 +21439,7 @@ vp-replace
} else {
$.open =
url => window.open(url, '_blank');
}
}
$.debounce = function(wait, fn) {
let lastCall = 0;
@ -21432,7 +21530,9 @@ vp-replace
:
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);

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': [
true,
`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': {
@ -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:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
*/
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() {
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
});
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
});
},
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};
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;
$$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 {
@ -10954,6 +11043,10 @@ textarea.copy-text-element {
}
/* File */
.expanded-image > .post > .file > .fileThumb {
display: flex;
flex-direction: column;
}
.fileText-original,
.fnswitch:hover > .fntrunc,
.fnswitch:not(:hover) > .fnfull,
@ -10968,6 +11061,11 @@ textarea.copy-text-element {
.expanded-image > .post > .file > .fileThumb > .full-image {
display: inline;
}
.expanded-image > .post > .file > .fileThumb > audio {
height: 30px;
width: 100%;
min-width: 300px;
}
.expanded-image {
clear: left;
}
@ -21230,7 +21328,7 @@ vp-replace
} else {
$.open =
url => window.open(url, '_blank');
}
}
$.debounce = function(wait, fn) {
let lastCall = 0;
@ -21321,7 +21419,9 @@ vp-replace
:
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);

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 ImageCommon from "./ImageCommon";
import Volume from "./Volume";
import Audio from "./Audio";
import type { default as Post, PostClone } from "../classes/Post";
/*
* decaffeinate suggestions:
@ -39,7 +41,7 @@ var ImageExpand = {
});
},
node() {
node(this: Post | PostClone) {
if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; }
$.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
@ -202,22 +204,32 @@ var ImageExpand = {
}
if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); }
delete file.fullImage;
return $.queueTask(function() {
$.queueTask(function() {
// XXX Work around Chrome/Chromium not firing mouseover on the thumbnail.
if (file.isExpanding || file.isExpanded) { return; }
$.rmClass(el, 'full-image');
if (el.id) { return; }
return $.rm(el);
});
if (file.audio) {
file.audio.remove();
delete file.audio;
if (file.audioSlider) {
file.audioSlider.remove();
delete file.audioSlider;
}
}
},
expand(post, src) {
// Do not expand images of hidden/filtered replies, or already expanded pictures.
let el;
expand(post: Post, src?: string) {
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; }
let el: HTMLImageElement| HTMLVideoElement;
$.addClass(thumb, 'expanding');
file.isExpanding = true;
@ -255,11 +267,33 @@ var ImageExpand = {
}
if (!isVideo) {
return $.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
$.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
} else if (el.readyState >= el.HAVE_METADATA) {
return ImageExpand.completeExpand(post);
ImageExpand.completeExpand(post);
} 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;
if (!playing) {
fullImage.controls = controls;

View File

@ -7,6 +7,25 @@ import type Board from "./Board";
import Callbacks from "./Callbacks";
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 {
declare root: HTMLElement;
declare thread: Thread;
@ -291,15 +310,7 @@ export default class Post {
}
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 };
for (var key in g.SITE.selectors.file) {

View File

@ -324,7 +324,11 @@ const Config = {
'Volume in New Tab': [
true,
`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': {

View File

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

View File

@ -465,7 +465,7 @@ if (!globalThis.chrome?.extension) {
} else {
$.open =
url => window.open(url, '_blank');
}
}
$.debounce = function(wait, fn) {
let lastCall = 0;
@ -556,7 +556,9 @@ $.minmax = (value, min, max) => value < min ?
:
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);