4chan-x/src/Images/ImageExpand.js
ccd0 98dcd7cd34 Rename files from .coffee to .js.
for f in src/*/*.coffee; do git mv -- "$f" "${f/.coffee/.js}"; done
2023-03-13 13:07:48 -07:00

386 lines
12 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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 = $.el('a', {
className: 'expand-all-shortcut fa fa-expand',
textContent: 'EAI',
title: 'Expand All Images',
href: 'javascript:;'
}
);
$.on(this.EAI, 'click', this.cb.toggleAll);
Header.addShortcut('expand-all', this.EAI, 520);
$.on(d, 'scroll visibilitychange', this.cb.playVideos);
this.videoControls = $.el('span', {className: 'video-controls'});
$.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; }
$.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 ($.modifiedClick(e)) { return; }
const post = Get.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;
$.event('CloseMenu');
const threadRoot = Nav.getThread();
const toggle = function(post) {
const {file} = post;
if (!file || (!file.isImage && !file.isVideo) || !doc.contains(post.nodes.root)) { return; }
if (ImageExpand.on &&
((!Conf['Expand spoilers'] && file.isSpoiler) ||
(!Conf['Expand videos'] && file.isVideo) ||
(Conf['Expand from here'] && (Header.getTopOf(file.thumb) < 0)) ||
(Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) {
return;
}
return $.queueTask(func, post);
};
if (ImageExpand.on = $.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) {
ImageExpand.EAI.className = 'contract-all-shortcut fa fa-compress';
ImageExpand.EAI.title = 'Contract All Images';
func = ImageExpand.expand;
} else {
ImageExpand.EAI.className = 'expand-all-shortcut fa fa-expand';
ImageExpand.EAI.title = 'Expand All Images';
func = ImageExpand.contract;
}
return g.posts.forEach(function(post) {
for (post of [post, ...Array.from(post.clones)]) { toggle(post); }
});
},
playVideos() {
return g.posts.forEach(function(post) {
for (post of [post, ...Array.from(post.clones)]) {
var {file} = post;
if (!file || !file.isVideo || !file.isExpanded) { continue; }
var video = file.fullImage;
var visible = ($.hasAudio(video) && !video.muted) || Header.isNodeVisible(video);
if (visible && file.wasPlaying) {
delete file.wasPlaying;
video.play();
} else if (!visible && !video.paused) {
file.wasPlaying = true;
video.pause();
}
}
});
},
setFitness() {
return $[this.checked ? 'addClass' : 'rmClass'](doc, 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 = $.x("following::div[contains(@class,'postContainer')][1]", next))) {
if (!$('.stub', next) && (next.offsetHeight !== 0)) { break; }
}
if (next) {
return Header.scrollTo(next);
}
}
},
contract(post) {
let bottom, el, oldHeight, scrollY;
const {file} = post;
if (el = file.fullImage) {
const top = Header.getTopOf(el);
bottom = top + el.getBoundingClientRect().height;
oldHeight = d.body.clientHeight;
({scrollY} = window);
}
$.rmClass(post.nodes.root, 'expanded-image');
$.rmClass(file.thumb, 'expanding');
$.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.contains(el)) {
if (bottom <= 0) {
// For images entirely above us, scroll to remain in place.
window.scrollBy(0, ((scrollY - window.scrollY) + d.body.clientHeight) - oldHeight);
} else {
// For images not above us that would be moved above us, scroll to the thumbnail.
Header.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);
}
}
$.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];
$.off(el, eventName, cb);
}
}
if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); }
delete file.fullImage;
return $.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);
});
},
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; }
$.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());
$.on(el, 'error', ImageExpand.error);
if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { ImageCommon.rewind(el); }
el.removeAttribute('id');
} else {
el = (file.fullImage = $.el((isVideo ? 'video' : 'img')));
el.dataset.fileID = `${post.fullID}.${file.index}`;
$.on(el, 'error', ImageExpand.error);
el.src = src || file.url;
}
el.className = 'full-image';
$.after(thumb, el);
if (isVideo) {
// add contract link to file info
if (!file.videoControls) {
file.videoControls = ImageExpand.videoControls.cloneNode(true);
$.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 $.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post));
} else if (el.readyState >= el.HAVE_METADATA) {
return ImageExpand.completeExpand(post);
} else {
return $.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post));
}
},
completeExpand(post) {
const {file} = post;
if (!file.isExpanding) { return; } // contracted before the image loaded
const bottom = Header.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height;
const oldHeight = d.body.clientHeight;
const {scrollY} = window;
$.addClass(post.nodes.root, 'expanded-image');
$.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.contains(post.nodes.root) && (bottom <= 0)) {
window.scrollBy(0, ((scrollY - window.scrollY) + d.body.clientHeight) - oldHeight);
}
// Scroll to display full image.
if (file.scrollIntoView) {
delete file.scrollIntoView;
const imageBottom = Math.min(doc.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header.getBottomOf(file.fullImage));
if (imageBottom < 0) {
window.scrollBy(0, Math.min(-imageBottom, Header.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;
$.asap((() => doc.contains(fullImage)), function() {
if (!d.hidden && Header.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.postFromNode(this)); } }
};
})(),
setupVideoCB(post) {
for (var eventName in ImageExpand.videoCB) {
var cb = ImageExpand.videoCB[eventName];
$.on(post.file.fullImage, eventName, cb);
}
if (post.file.videoControls) {
return $.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post));
}
},
error() {
const post = Get.postFromNode(this);
$.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 = $.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.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)) {
$.on(input, 'change', ImageExpand.cb.setFitness);
}
$.event('change', null, input);
$.on(input, 'change', $.cb.checked);
return {el: label};
}
}
};