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