Audio posts
Not yet available int the gallery. Ill add that if someone asks for it.
This commit is contained in:
parent
e4236c62c8
commit
7d6dd7c653
@ -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);
|
||||
|
||||
|
||||
802
builds/4chan-XT-noupdate.user.min.js
vendored
802
builds/4chan-XT-noupdate.user.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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
58
src/Images/Audio.ts
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user