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,6 +3382,55 @@ 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
@ -3385,73 +3438,76 @@ https://*.hcaptcha.com
*/ */
var ImageExpand = { var ImageExpand = {
init() { init() {
if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { return; } if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) {
return;
}
this.EAI = $$1.el('a', { this.EAI = $$1.el('a', {
className: 'expand-all-shortcut', className: 'expand-all-shortcut',
textContent: '', textContent: '',
title: 'Expand All Images', title: 'Expand All Images',
href: 'javascript:;' href: 'javascript:;'
} });
);
$$1.on(this.EAI, 'click', this.cb.toggleAll); $$1.on(this.EAI, 'click', this.cb.toggleAll);
Header$1.addShortcut('expand-all', this.EAI, 520); Header$1.addShortcut('expand-all', this.EAI, 520);
$$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos); $$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos);
this.videoControls = $$1.el('span', {className: 'video-controls'}); 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>"}); $$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 Callbacks.Post.push({
name: 'Image Expansion', name: 'Image Expansion',
cb: this.node cb: this.node
}); });
}, },
node() { node() {
if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; } if (!this.file || (!this.file.isImage && !this.file.isVideo)) {
return;
}
$$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle); $$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
if (this.isClone) { if (this.isClone) {
if (this.file.isExpanding) { if (this.file.isExpanding) {
// If we clone a post where the image is still loading, // If we clone a post where the image is still loading,
// make it loading in the clone too. // make it loading in the clone too.
ImageExpand.contract(this); ImageExpand.contract(this);
return ImageExpand.expand(this); return ImageExpand.expand(this);
}
} else if (this.file.isExpanded && this.file.isVideo) { else if (this.file.isExpanded && this.file.isVideo) {
Volume.setup(this.file.fullImage); Volume.setup(this.file.fullImage);
ImageExpand.setupVideoCB(this); ImageExpand.setupVideoCB(this);
return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls); 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 && else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote &&
(Conf['Expand spoilers'] || !this.file.isSpoiler) && (Conf['Expand spoilers'] || !this.file.isSpoiler) &&
(Conf['Expand videos'] || !this.file.isVideo)) { (Conf['Expand videos'] || !this.file.isVideo)) {
return ImageExpand.expand(this); return ImageExpand.expand(this);
} }
}, },
cb: { cb: {
toggle(e) { toggle(e) {
if ($$1.modifiedClick(e)) { return; } if ($$1.modifiedClick(e)) {
return;
}
const post = Get$1.postFromNode(this); const post = Get$1.postFromNode(this);
const {file} = post; const { file } = post;
if (file.isExpanded && ImageCommon.onControls(e)) { return; } if (file.isExpanded && ImageCommon.onControls(e)) {
return;
}
e.preventDefault(); e.preventDefault();
if (!Conf['Autoplay'] && file.fullImage?.paused) { if (!Conf['Autoplay'] && file.fullImage?.paused) {
return file.fullImage.play(); return file.fullImage.play();
} else { }
else {
return ImageExpand.toggle(post); return ImageExpand.toggle(post);
} }
}, },
toggleAll() { toggleAll() {
let func; let func;
$$1.event('CloseMenu'); $$1.event('CloseMenu');
const threadRoot = Nav.getThread(); const threadRoot = Nav.getThread();
const toggle = function(post) { const toggle = function (post) {
const {file} = post; const { file } = post;
if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) { return; } if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) {
return;
}
if (ImageExpand.on && if (ImageExpand.on &&
((!Conf['Expand spoilers'] && file.isSpoiler) || ((!Conf['Expand spoilers'] && file.isSpoiler) ||
(!Conf['Expand videos'] && file.isVideo) || (!Conf['Expand videos'] && file.isVideo) ||
@ -3461,79 +3517,76 @@ https://*.hcaptcha.com
} }
return $$1.queueTask(func, post); return $$1.queueTask(func, post);
}; };
if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) { if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) {
ImageExpand.EAI.className = 'contract-all-shortcut'; ImageExpand.EAI.className = 'contract-all-shortcut';
ImageExpand.EAI.title = 'Contract All Images'; ImageExpand.EAI.title = 'Contract All Images';
ImageExpand.EAI.textContent = ''; ImageExpand.EAI.textContent = '';
func = ImageExpand.expand; func = ImageExpand.expand;
} else { }
else {
ImageExpand.EAI.className = 'expand-all-shortcut'; ImageExpand.EAI.className = 'expand-all-shortcut';
ImageExpand.EAI.title = 'Expand All Images'; ImageExpand.EAI.title = 'Expand All Images';
ImageExpand.EAI.textContent = ''; ImageExpand.EAI.textContent = '';
func = ImageExpand.contract; func = ImageExpand.contract;
} }
return g.posts.forEach(function (post) {
return g.posts.forEach(function(post) { for (post of [post, ...post.clones]) {
for (post of [post, ...post.clones]) { toggle(post); } toggle(post);
}
}); });
}, },
playVideos() { playVideos() {
return g.posts.forEach(function(post) { return g.posts.forEach(function (post) {
for (post of [post, ...post.clones]) { for (post of [post, ...post.clones]) {
var {file} = post; var { file } = post;
if (!file || !file.isVideo || !file.isExpanded) { continue; } if (!file || !file.isVideo || !file.isExpanded) {
continue;
}
var video = file.fullImage; var video = file.fullImage;
var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video); var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video);
if (visible && file.wasPlaying) { if (visible && file.wasPlaying) {
delete file.wasPlaying; delete file.wasPlaying;
video.play(); video.play();
} else if (!visible && !video.paused) { }
else if (!visible && !video.paused) {
file.wasPlaying = true; file.wasPlaying = true;
video.pause(); video.pause();
} }
} }
}); });
}, },
setFitness() { setFitness() {
return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-')); return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-'));
} }
}, },
toggle(post) { toggle(post) {
if (!post.file.isExpanding && !post.file.isExpanded) { if (!post.file.isExpanding && !post.file.isExpanded) {
post.file.scrollIntoView = Conf['Scroll into view']; post.file.scrollIntoView = Conf['Scroll into view'];
ImageExpand.expand(post); ImageExpand.expand(post);
return; return;
} }
ImageExpand.contract(post); ImageExpand.contract(post);
if (Conf['Advance on contract']) { if (Conf['Advance on contract']) {
let next = post.nodes.root; let next = post.nodes.root;
while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) { while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) {
if (!$$1('.stub', next) && (next.offsetHeight !== 0)) { break; } if (!$$1('.stub', next) && (next.offsetHeight !== 0)) {
break;
}
} }
if (next) { if (next) {
return Header$1.scrollTo(next); return Header$1.scrollTo(next);
} }
} }
}, },
contract(post) { contract(post) {
let bottom, el, oldHeight, scrollY; let bottom, el, oldHeight, scrollY;
const {file} = post; const { file } = post;
if (el = file.fullImage) { if (el = file.fullImage) {
const top = Header$1.getTopOf(el); const top = Header$1.getTopOf(el);
bottom = top + el.getBoundingClientRect().height; bottom = top + el.getBoundingClientRect().height;
oldHeight = d$1.body.clientHeight; oldHeight = d$1.body.clientHeight;
({scrollY} = window); ({ scrollY } = window);
} }
$$1.rmClass(post.nodes.root, 'expanded-image'); $$1.rmClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding'); $$1.rmClass(file.thumb, 'expanding');
$$1.rm(file.videoControls); $$1.rm(file.videoControls);
@ -3542,14 +3595,15 @@ https://*.hcaptcha.com
for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) { for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) {
delete file[x]; delete file[x];
} }
if (!el) {
if (!el) { return; } return;
}
if (doc$1.contains(el)) { if (doc$1.contains(el)) {
if (bottom <= 0) { if (bottom <= 0) {
// For images entirely above us, scroll to remain in place. // For images entirely above us, scroll to remain in place.
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
} else { }
else {
// For images not above us that would be moved above us, scroll to the thumbnail. // For images not above us that would be moved above us, scroll to the thumbnail.
Header$1.scrollToIfNeeded(post.nodes.root); Header$1.scrollToIfNeeded(post.nodes.root);
} }
@ -3558,7 +3612,6 @@ https://*.hcaptcha.com
window.scrollBy(-window.scrollX, 0); window.scrollBy(-window.scrollX, 0);
} }
} }
$$1.off(el, 'error', ImageExpand.error); $$1.off(el, 'error', ImageExpand.error);
ImageCommon.pushCache(el); ImageCommon.pushCache(el);
if (file.isVideo) { if (file.isVideo) {
@ -3568,87 +3621,121 @@ https://*.hcaptcha.com
$$1.off(el, eventName, cb); $$1.off(el, eventName, cb);
} }
} }
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 $$1.queueTask(function() { $$1.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;
}
$$1.rmClass(el, 'full-image'); $$1.rmClass(el, 'full-image');
if (el.id) { return; } if (el.id) {
return;
}
return $$1.rm(el); 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) { expand(post, src) {
const { file } = post;
const { thumb, thumbLink, isVideo } = file;
// Do not expand images of hidden/filtered replies, or already expanded pictures. // Do not expand images of hidden/filtered replies, or already expanded pictures.
if (post.isHidden || file.isExpanding || file.isExpanded) {
return;
}
let el; let el;
const {file} = post;
const {thumb, thumbLink, isVideo} = file;
if (post.isHidden || file.isExpanding || file.isExpanded) { return; }
$$1.addClass(thumb, 'expanding'); $$1.addClass(thumb, 'expanding');
file.isExpanding = true; file.isExpanding = true;
if (file.fullImage) { if (file.fullImage) {
el = file.fullImage; el = file.fullImage;
} else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) { }
else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) {
el = (file.fullImage = ImageCommon.popCache()); el = (file.fullImage = ImageCommon.popCache());
$$1.on(el, 'error', ImageExpand.error); $$1.on(el, 'error', ImageExpand.error);
if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { ImageCommon.rewind(el); } if (Conf['Restart when Opened'] && (el.id !== 'ihover')) {
ImageCommon.rewind(el);
}
el.removeAttribute('id'); el.removeAttribute('id');
} else { }
else {
el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img'))); el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img')));
el.dataset.fileID = `${post.fullID}.${file.index}`; el.dataset.fileID = `${post.fullID}.${file.index}`;
$$1.on(el, 'error', ImageExpand.error); $$1.on(el, 'error', ImageExpand.error);
el.src = src || file.url; el.src = src || file.url;
} }
el.className = 'full-image'; el.className = 'full-image';
$$1.after(thumb, el); $$1.after(thumb, el);
if (isVideo) { if (isVideo) {
// add contract link to file info // add contract link to file info
if (!file.videoControls) { if (!file.videoControls) {
file.videoControls = ImageExpand.videoControls.cloneNode(true); file.videoControls = ImageExpand.videoControls.cloneNode(true);
$$1.add(file.text, file.videoControls); $$1.add(file.text, file.videoControls);
} }
// disable link to file so native controls can work // disable link to file so native controls can work
thumbLink.removeAttribute('href'); thumbLink.removeAttribute('href');
thumbLink.removeAttribute('target'); thumbLink.removeAttribute('target');
el.loop = true; el.loop = true;
Volume.setup(el); Volume.setup(el);
ImageExpand.setupVideoCB(post); ImageExpand.setupVideoCB(post);
} }
if (!isVideo) { if (!isVideo) {
return $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post)); $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
} else if (el.readyState >= el.HAVE_METADATA) { }
return ImageExpand.completeExpand(post); else if (el.readyState >= el.HAVE_METADATA) {
} else { ImageExpand.completeExpand(post);
return $$1.on(el, 'loadedmetadata', () => 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) { completeExpand(post) {
const {file} = post; const { file } = post;
if (!file.isExpanding) { return; } // contracted before the image loaded if (!file.isExpanding) {
return;
} // contracted before the image loaded
const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height;
const oldHeight = d$1.body.clientHeight; const oldHeight = d$1.body.clientHeight;
const {scrollY} = window; const { scrollY } = window;
$$1.addClass(post.nodes.root, 'expanded-image'); $$1.addClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding'); $$1.rmClass(file.thumb, 'expanding');
file.isExpanded = true; file.isExpanded = true;
delete file.isExpanding; delete file.isExpanding;
// Scroll to keep our place in the thread when images are expanded above us. // Scroll to keep our place in the thread when images are expanded above us.
if (doc$1.contains(post.nodes.root) && (bottom <= 0)) { if (doc$1.contains(post.nodes.root) && (bottom <= 0)) {
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
} }
// Scroll to display full image. // Scroll to display full image.
if (file.scrollIntoView) { if (file.scrollIntoView) {
delete file.scrollIntoView; delete file.scrollIntoView;
@ -3657,23 +3744,22 @@ https://*.hcaptcha.com
window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage))); window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage)));
} }
} }
if (file.isVideo) { if (file.isVideo) {
return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']); return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']);
} }
}, },
setupVideo(post, playing, controls) { setupVideo(post, playing, controls) {
const {fullImage} = post.file; const { fullImage } = post.file;
if (!playing) { if (!playing) {
fullImage.controls = controls; fullImage.controls = controls;
return; return;
} }
fullImage.controls = false; fullImage.controls = false;
$$1.asap((() => doc$1.contains(fullImage)), function() { $$1.asap((() => doc$1.contains(fullImage)), function () {
if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) { if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) {
return fullImage.play(); return fullImage.play();
} else { }
else {
return post.file.wasPlaying = true; return post.file.wasPlaying = true;
} }
}); });
@ -3681,18 +3767,22 @@ https://*.hcaptcha.com
return ImageCommon.addControls(fullImage); return ImageCommon.addControls(fullImage);
} }
}, },
videoCB: (function () {
videoCB: (function() {
// dragging to the left contracts the video // dragging to the left contracts the video
let mousedown = false; let mousedown = false;
return { return {
mouseover() { return mousedown = false; }, mouseover() { return mousedown = false; },
mousedown(e) { if (e.button === 0) { return mousedown = true; } }, mousedown(e) { if (e.button === 0) {
mouseup(e) { if (e.button === 0) { return mousedown = false; } }, return mousedown = true;
mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) { return ImageExpand.toggle(Get$1.postFromNode(this)); } } } },
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) { setupVideoCB(post) {
for (var eventName in ImageExpand.videoCB) { for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName]; var cb = ImageExpand.videoCB[eventName];
@ -3702,7 +3792,6 @@ https://*.hcaptcha.com
return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post)); return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post));
} }
}, },
error() { error() {
const post = Get$1.postFromNode(this); const post = Get$1.postFromNode(this);
$$1.rm(this); $$1.rm(this);
@ -3711,7 +3800,9 @@ https://*.hcaptcha.com
// - before the image started loading. // - before the image started loading.
// - after the image started loading. // - after the image started loading.
// Don't try to re-expand if it was already contracted. // Don't try to re-expand if it was already contracted.
if (!post.file.isExpanding && !post.file.isExpanded) { return; } if (!post.file.isExpanding && !post.file.isExpanded) {
return;
}
if (ImageCommon.decodeError(this, post.file)) { if (ImageCommon.decodeError(this, post.file)) {
return ImageExpand.contract(post); return ImageExpand.contract(post);
} }
@ -3719,38 +3810,36 @@ https://*.hcaptcha.com
if (ImageCommon.isFromArchive(this)) { if (ImageCommon.isFromArchive(this)) {
return ImageExpand.contract(post); return ImageExpand.contract(post);
} }
return ImageCommon.error(this, post, post.file, 10 * SECOND, function(URL) { return ImageCommon.error(this, post, post.file, 10 * SECOND, function (URL) {
if (post.file.isExpanding || post.file.isExpanded) { if (post.file.isExpanding || post.file.isExpanded) {
ImageExpand.contract(post); ImageExpand.contract(post);
if (URL) { return ImageExpand.expand(post, URL); } if (URL) {
return ImageExpand.expand(post, URL);
}
} }
}); });
}, },
menu: { menu: {
init() { init() {
if (!ImageExpand.enabled) { return; } if (!ImageExpand.enabled) {
return;
}
const el = $$1.el('span', { const el = $$1.el('span', {
textContent: 'Image Expansion', textContent: 'Image Expansion',
className: 'image-expansion-link' className: 'image-expansion-link'
} });
); const { createSubEntry } = ImageExpand.menu;
const {createSubEntry} = ImageExpand.menu;
const subEntries = []; const subEntries = [];
for (var name in Config.imageExpansion) { for (var name in Config.imageExpansion) {
var conf = Config.imageExpansion[name]; var conf = Config.imageExpansion[name];
subEntries.push(createSubEntry(name, conf[1])); subEntries.push(createSubEntry(name, conf[1]));
} }
return Header$1.menu.addEntry({ return Header$1.menu.addEntry({
el, el,
order: 105, order: 105,
subEntries subEntries
}); });
}, },
createSubEntry(name, desc) { createSubEntry(name, desc) {
const label = UI.checkbox(name, name); const label = UI.checkbox(name, name);
label.title = desc; label.title = desc;
@ -3760,7 +3849,7 @@ https://*.hcaptcha.com
} }
$$1.event('change', null, input); $$1.event('change', null, input);
$$1.on(input, 'change', $$1.cb.checked); $$1.on(input, 'change', $$1.cb.checked);
return {el: label}; return { el: label };
} }
} }
}; };
@ -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;
} }
@ -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,6 +3271,55 @@ 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
@ -3274,73 +3327,76 @@ https://*.hcaptcha.com
*/ */
var ImageExpand = { var ImageExpand = {
init() { init() {
if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { return; } if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) {
return;
}
this.EAI = $$1.el('a', { this.EAI = $$1.el('a', {
className: 'expand-all-shortcut', className: 'expand-all-shortcut',
textContent: '', textContent: '',
title: 'Expand All Images', title: 'Expand All Images',
href: 'javascript:;' href: 'javascript:;'
} });
);
$$1.on(this.EAI, 'click', this.cb.toggleAll); $$1.on(this.EAI, 'click', this.cb.toggleAll);
Header$1.addShortcut('expand-all', this.EAI, 520); Header$1.addShortcut('expand-all', this.EAI, 520);
$$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos); $$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos);
this.videoControls = $$1.el('span', {className: 'video-controls'}); 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>"}); $$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 Callbacks.Post.push({
name: 'Image Expansion', name: 'Image Expansion',
cb: this.node cb: this.node
}); });
}, },
node() { node() {
if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; } if (!this.file || (!this.file.isImage && !this.file.isVideo)) {
return;
}
$$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle); $$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle);
if (this.isClone) { if (this.isClone) {
if (this.file.isExpanding) { if (this.file.isExpanding) {
// If we clone a post where the image is still loading, // If we clone a post where the image is still loading,
// make it loading in the clone too. // make it loading in the clone too.
ImageExpand.contract(this); ImageExpand.contract(this);
return ImageExpand.expand(this); return ImageExpand.expand(this);
}
} else if (this.file.isExpanded && this.file.isVideo) { else if (this.file.isExpanded && this.file.isVideo) {
Volume.setup(this.file.fullImage); Volume.setup(this.file.fullImage);
ImageExpand.setupVideoCB(this); ImageExpand.setupVideoCB(this);
return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls); 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 && else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote &&
(Conf['Expand spoilers'] || !this.file.isSpoiler) && (Conf['Expand spoilers'] || !this.file.isSpoiler) &&
(Conf['Expand videos'] || !this.file.isVideo)) { (Conf['Expand videos'] || !this.file.isVideo)) {
return ImageExpand.expand(this); return ImageExpand.expand(this);
} }
}, },
cb: { cb: {
toggle(e) { toggle(e) {
if ($$1.modifiedClick(e)) { return; } if ($$1.modifiedClick(e)) {
return;
}
const post = Get$1.postFromNode(this); const post = Get$1.postFromNode(this);
const {file} = post; const { file } = post;
if (file.isExpanded && ImageCommon.onControls(e)) { return; } if (file.isExpanded && ImageCommon.onControls(e)) {
return;
}
e.preventDefault(); e.preventDefault();
if (!Conf['Autoplay'] && file.fullImage?.paused) { if (!Conf['Autoplay'] && file.fullImage?.paused) {
return file.fullImage.play(); return file.fullImage.play();
} else { }
else {
return ImageExpand.toggle(post); return ImageExpand.toggle(post);
} }
}, },
toggleAll() { toggleAll() {
let func; let func;
$$1.event('CloseMenu'); $$1.event('CloseMenu');
const threadRoot = Nav.getThread(); const threadRoot = Nav.getThread();
const toggle = function(post) { const toggle = function (post) {
const {file} = post; const { file } = post;
if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) { return; } if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) {
return;
}
if (ImageExpand.on && if (ImageExpand.on &&
((!Conf['Expand spoilers'] && file.isSpoiler) || ((!Conf['Expand spoilers'] && file.isSpoiler) ||
(!Conf['Expand videos'] && file.isVideo) || (!Conf['Expand videos'] && file.isVideo) ||
@ -3350,79 +3406,76 @@ https://*.hcaptcha.com
} }
return $$1.queueTask(func, post); return $$1.queueTask(func, post);
}; };
if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) { if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) {
ImageExpand.EAI.className = 'contract-all-shortcut'; ImageExpand.EAI.className = 'contract-all-shortcut';
ImageExpand.EAI.title = 'Contract All Images'; ImageExpand.EAI.title = 'Contract All Images';
ImageExpand.EAI.textContent = ''; ImageExpand.EAI.textContent = '';
func = ImageExpand.expand; func = ImageExpand.expand;
} else { }
else {
ImageExpand.EAI.className = 'expand-all-shortcut'; ImageExpand.EAI.className = 'expand-all-shortcut';
ImageExpand.EAI.title = 'Expand All Images'; ImageExpand.EAI.title = 'Expand All Images';
ImageExpand.EAI.textContent = ''; ImageExpand.EAI.textContent = '';
func = ImageExpand.contract; func = ImageExpand.contract;
} }
return g.posts.forEach(function (post) {
return g.posts.forEach(function(post) { for (post of [post, ...post.clones]) {
for (post of [post, ...post.clones]) { toggle(post); } toggle(post);
}
}); });
}, },
playVideos() { playVideos() {
return g.posts.forEach(function(post) { return g.posts.forEach(function (post) {
for (post of [post, ...post.clones]) { for (post of [post, ...post.clones]) {
var {file} = post; var { file } = post;
if (!file || !file.isVideo || !file.isExpanded) { continue; } if (!file || !file.isVideo || !file.isExpanded) {
continue;
}
var video = file.fullImage; var video = file.fullImage;
var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video); var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video);
if (visible && file.wasPlaying) { if (visible && file.wasPlaying) {
delete file.wasPlaying; delete file.wasPlaying;
video.play(); video.play();
} else if (!visible && !video.paused) { }
else if (!visible && !video.paused) {
file.wasPlaying = true; file.wasPlaying = true;
video.pause(); video.pause();
} }
} }
}); });
}, },
setFitness() { setFitness() {
return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-')); return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-'));
} }
}, },
toggle(post) { toggle(post) {
if (!post.file.isExpanding && !post.file.isExpanded) { if (!post.file.isExpanding && !post.file.isExpanded) {
post.file.scrollIntoView = Conf['Scroll into view']; post.file.scrollIntoView = Conf['Scroll into view'];
ImageExpand.expand(post); ImageExpand.expand(post);
return; return;
} }
ImageExpand.contract(post); ImageExpand.contract(post);
if (Conf['Advance on contract']) { if (Conf['Advance on contract']) {
let next = post.nodes.root; let next = post.nodes.root;
while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) { while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) {
if (!$$1('.stub', next) && (next.offsetHeight !== 0)) { break; } if (!$$1('.stub', next) && (next.offsetHeight !== 0)) {
break;
}
} }
if (next) { if (next) {
return Header$1.scrollTo(next); return Header$1.scrollTo(next);
} }
} }
}, },
contract(post) { contract(post) {
let bottom, el, oldHeight, scrollY; let bottom, el, oldHeight, scrollY;
const {file} = post; const { file } = post;
if (el = file.fullImage) { if (el = file.fullImage) {
const top = Header$1.getTopOf(el); const top = Header$1.getTopOf(el);
bottom = top + el.getBoundingClientRect().height; bottom = top + el.getBoundingClientRect().height;
oldHeight = d$1.body.clientHeight; oldHeight = d$1.body.clientHeight;
({scrollY} = window); ({ scrollY } = window);
} }
$$1.rmClass(post.nodes.root, 'expanded-image'); $$1.rmClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding'); $$1.rmClass(file.thumb, 'expanding');
$$1.rm(file.videoControls); $$1.rm(file.videoControls);
@ -3431,14 +3484,15 @@ https://*.hcaptcha.com
for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) { for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) {
delete file[x]; delete file[x];
} }
if (!el) {
if (!el) { return; } return;
}
if (doc$1.contains(el)) { if (doc$1.contains(el)) {
if (bottom <= 0) { if (bottom <= 0) {
// For images entirely above us, scroll to remain in place. // For images entirely above us, scroll to remain in place.
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
} else { }
else {
// For images not above us that would be moved above us, scroll to the thumbnail. // For images not above us that would be moved above us, scroll to the thumbnail.
Header$1.scrollToIfNeeded(post.nodes.root); Header$1.scrollToIfNeeded(post.nodes.root);
} }
@ -3447,7 +3501,6 @@ https://*.hcaptcha.com
window.scrollBy(-window.scrollX, 0); window.scrollBy(-window.scrollX, 0);
} }
} }
$$1.off(el, 'error', ImageExpand.error); $$1.off(el, 'error', ImageExpand.error);
ImageCommon.pushCache(el); ImageCommon.pushCache(el);
if (file.isVideo) { if (file.isVideo) {
@ -3457,87 +3510,121 @@ https://*.hcaptcha.com
$$1.off(el, eventName, cb); $$1.off(el, eventName, cb);
} }
} }
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 $$1.queueTask(function() { $$1.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;
}
$$1.rmClass(el, 'full-image'); $$1.rmClass(el, 'full-image');
if (el.id) { return; } if (el.id) {
return;
}
return $$1.rm(el); 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) { expand(post, src) {
const { file } = post;
const { thumb, thumbLink, isVideo } = file;
// Do not expand images of hidden/filtered replies, or already expanded pictures. // Do not expand images of hidden/filtered replies, or already expanded pictures.
if (post.isHidden || file.isExpanding || file.isExpanded) {
return;
}
let el; let el;
const {file} = post;
const {thumb, thumbLink, isVideo} = file;
if (post.isHidden || file.isExpanding || file.isExpanded) { return; }
$$1.addClass(thumb, 'expanding'); $$1.addClass(thumb, 'expanding');
file.isExpanding = true; file.isExpanding = true;
if (file.fullImage) { if (file.fullImage) {
el = file.fullImage; el = file.fullImage;
} else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) { }
else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) {
el = (file.fullImage = ImageCommon.popCache()); el = (file.fullImage = ImageCommon.popCache());
$$1.on(el, 'error', ImageExpand.error); $$1.on(el, 'error', ImageExpand.error);
if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { ImageCommon.rewind(el); } if (Conf['Restart when Opened'] && (el.id !== 'ihover')) {
ImageCommon.rewind(el);
}
el.removeAttribute('id'); el.removeAttribute('id');
} else { }
else {
el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img'))); el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img')));
el.dataset.fileID = `${post.fullID}.${file.index}`; el.dataset.fileID = `${post.fullID}.${file.index}`;
$$1.on(el, 'error', ImageExpand.error); $$1.on(el, 'error', ImageExpand.error);
el.src = src || file.url; el.src = src || file.url;
} }
el.className = 'full-image'; el.className = 'full-image';
$$1.after(thumb, el); $$1.after(thumb, el);
if (isVideo) { if (isVideo) {
// add contract link to file info // add contract link to file info
if (!file.videoControls) { if (!file.videoControls) {
file.videoControls = ImageExpand.videoControls.cloneNode(true); file.videoControls = ImageExpand.videoControls.cloneNode(true);
$$1.add(file.text, file.videoControls); $$1.add(file.text, file.videoControls);
} }
// disable link to file so native controls can work // disable link to file so native controls can work
thumbLink.removeAttribute('href'); thumbLink.removeAttribute('href');
thumbLink.removeAttribute('target'); thumbLink.removeAttribute('target');
el.loop = true; el.loop = true;
Volume.setup(el); Volume.setup(el);
ImageExpand.setupVideoCB(post); ImageExpand.setupVideoCB(post);
} }
if (!isVideo) { if (!isVideo) {
return $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post)); $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
} else if (el.readyState >= el.HAVE_METADATA) { }
return ImageExpand.completeExpand(post); else if (el.readyState >= el.HAVE_METADATA) {
} else { ImageExpand.completeExpand(post);
return $$1.on(el, 'loadedmetadata', () => 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) { completeExpand(post) {
const {file} = post; const { file } = post;
if (!file.isExpanding) { return; } // contracted before the image loaded if (!file.isExpanding) {
return;
} // contracted before the image loaded
const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height;
const oldHeight = d$1.body.clientHeight; const oldHeight = d$1.body.clientHeight;
const {scrollY} = window; const { scrollY } = window;
$$1.addClass(post.nodes.root, 'expanded-image'); $$1.addClass(post.nodes.root, 'expanded-image');
$$1.rmClass(file.thumb, 'expanding'); $$1.rmClass(file.thumb, 'expanding');
file.isExpanded = true; file.isExpanded = true;
delete file.isExpanding; delete file.isExpanding;
// Scroll to keep our place in the thread when images are expanded above us. // Scroll to keep our place in the thread when images are expanded above us.
if (doc$1.contains(post.nodes.root) && (bottom <= 0)) { if (doc$1.contains(post.nodes.root) && (bottom <= 0)) {
window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight);
} }
// Scroll to display full image. // Scroll to display full image.
if (file.scrollIntoView) { if (file.scrollIntoView) {
delete file.scrollIntoView; delete file.scrollIntoView;
@ -3546,23 +3633,22 @@ https://*.hcaptcha.com
window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage))); window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage)));
} }
} }
if (file.isVideo) { if (file.isVideo) {
return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']); return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']);
} }
}, },
setupVideo(post, playing, controls) { setupVideo(post, playing, controls) {
const {fullImage} = post.file; const { fullImage } = post.file;
if (!playing) { if (!playing) {
fullImage.controls = controls; fullImage.controls = controls;
return; return;
} }
fullImage.controls = false; fullImage.controls = false;
$$1.asap((() => doc$1.contains(fullImage)), function() { $$1.asap((() => doc$1.contains(fullImage)), function () {
if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) { if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) {
return fullImage.play(); return fullImage.play();
} else { }
else {
return post.file.wasPlaying = true; return post.file.wasPlaying = true;
} }
}); });
@ -3570,18 +3656,22 @@ https://*.hcaptcha.com
return ImageCommon.addControls(fullImage); return ImageCommon.addControls(fullImage);
} }
}, },
videoCB: (function () {
videoCB: (function() {
// dragging to the left contracts the video // dragging to the left contracts the video
let mousedown = false; let mousedown = false;
return { return {
mouseover() { return mousedown = false; }, mouseover() { return mousedown = false; },
mousedown(e) { if (e.button === 0) { return mousedown = true; } }, mousedown(e) { if (e.button === 0) {
mouseup(e) { if (e.button === 0) { return mousedown = false; } }, return mousedown = true;
mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) { return ImageExpand.toggle(Get$1.postFromNode(this)); } } } },
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) { setupVideoCB(post) {
for (var eventName in ImageExpand.videoCB) { for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName]; var cb = ImageExpand.videoCB[eventName];
@ -3591,7 +3681,6 @@ https://*.hcaptcha.com
return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post)); return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post));
} }
}, },
error() { error() {
const post = Get$1.postFromNode(this); const post = Get$1.postFromNode(this);
$$1.rm(this); $$1.rm(this);
@ -3600,7 +3689,9 @@ https://*.hcaptcha.com
// - before the image started loading. // - before the image started loading.
// - after the image started loading. // - after the image started loading.
// Don't try to re-expand if it was already contracted. // Don't try to re-expand if it was already contracted.
if (!post.file.isExpanding && !post.file.isExpanded) { return; } if (!post.file.isExpanding && !post.file.isExpanded) {
return;
}
if (ImageCommon.decodeError(this, post.file)) { if (ImageCommon.decodeError(this, post.file)) {
return ImageExpand.contract(post); return ImageExpand.contract(post);
} }
@ -3608,38 +3699,36 @@ https://*.hcaptcha.com
if (ImageCommon.isFromArchive(this)) { if (ImageCommon.isFromArchive(this)) {
return ImageExpand.contract(post); return ImageExpand.contract(post);
} }
return ImageCommon.error(this, post, post.file, 10 * SECOND, function(URL) { return ImageCommon.error(this, post, post.file, 10 * SECOND, function (URL) {
if (post.file.isExpanding || post.file.isExpanded) { if (post.file.isExpanding || post.file.isExpanded) {
ImageExpand.contract(post); ImageExpand.contract(post);
if (URL) { return ImageExpand.expand(post, URL); } if (URL) {
return ImageExpand.expand(post, URL);
}
} }
}); });
}, },
menu: { menu: {
init() { init() {
if (!ImageExpand.enabled) { return; } if (!ImageExpand.enabled) {
return;
}
const el = $$1.el('span', { const el = $$1.el('span', {
textContent: 'Image Expansion', textContent: 'Image Expansion',
className: 'image-expansion-link' className: 'image-expansion-link'
} });
); const { createSubEntry } = ImageExpand.menu;
const {createSubEntry} = ImageExpand.menu;
const subEntries = []; const subEntries = [];
for (var name in Config.imageExpansion) { for (var name in Config.imageExpansion) {
var conf = Config.imageExpansion[name]; var conf = Config.imageExpansion[name];
subEntries.push(createSubEntry(name, conf[1])); subEntries.push(createSubEntry(name, conf[1]));
} }
return Header$1.menu.addEntry({ return Header$1.menu.addEntry({
el, el,
order: 105, order: 105,
subEntries subEntries
}); });
}, },
createSubEntry(name, desc) { createSubEntry(name, desc) {
const label = UI.checkbox(name, name); const label = UI.checkbox(name, name);
label.title = desc; label.title = desc;
@ -3649,7 +3738,7 @@ https://*.hcaptcha.com
} }
$$1.event('change', null, input); $$1.event('change', null, input);
$$1.on(input, 'change', $$1.cb.checked); $$1.on(input, 'change', $$1.cb.checked);
return {el: label}; return { el: label };
} }
} }
}; };
@ -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;
} }
@ -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

@ -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);