4chan-XZ/src/Images/ImageExpand.js
2023-04-22 05:40:54 +02:00

526 lines
14 KiB
JavaScript

import Callbacks from '../classes/Callbacks'
import Config from '../config/Config'
import Get from '../General/Get'
import Header from '../General/Header'
import UI from '../General/UI'
import { Conf, d, doc, g } from '../globals/globals'
import Nav from '../Miscellaneous/Nav'
import $ from '../platform/$'
import { SECOND } from '../platform/helpers'
import ImageCommon from './ImageCommon'
import Volume from './Volume'
/*
* 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(function () {
if (file.isExpanded) {
return ImageExpand.contract(post)
} else {
return ImageExpand.expand(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 }
},
},
}
export default ImageExpand