From 7d6dd7c6533abded9a5e0bf759679f64e38045e7 Mon Sep 17 00:00:00 2001 From: Tuxedo Takodachi Date: Sat, 24 Jun 2023 17:52:48 +0200 Subject: [PATCH] Audio posts Not yet available int the gallery. Ill add that if someone asks for it. --- builds/4chan-XT-noupdate.user.js | 874 +++++++++++++---------- builds/4chan-XT-noupdate.user.min.js | 802 ++++++++++----------- builds/4chan-XT-noupdate.user.min.js.map | 2 +- builds/crx/script.js | 858 ++++++++++++---------- src/Images/Audio.ts | 58 ++ src/Images/ImageExpand.ts | 54 +- src/classes/Post.ts | 29 +- src/config/Config.js | 6 +- src/css/style.css | 9 + src/platform/$.js | 6 +- 10 files changed, 1510 insertions(+), 1188 deletions(-) create mode 100644 src/Images/Audio.ts diff --git a/builds/4chan-XT-noupdate.user.js b/builds/4chan-XT-noupdate.user.js index dfa9765be..577b369d9 100644 --- a/builds/4chan-XT-noupdate.user.js +++ b/builds/4chan-XT-noupdate.user.js @@ -725,7 +725,11 @@ div.boardTitle { 'Volume in New Tab': [ true, `Apply ${meta.name} mute and volume settings to videos opened in their own tabs.` - ] + ], + 'Enable sound posts': [ + true, + 'Enable loading audio from [sound=] file names. This audio is fetched from third parties.' + ], }, 'Menu': { @@ -3378,391 +3382,476 @@ https://*.hcaptcha.com } }; - /* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ - var ImageExpand = { - init() { - if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { return; } + const Audio = { + /** Add event listeners for videos with audio from a third party */ + setupSync(video, audio) { + video.addEventListener('playing', () => { + audio.currentTime = video.currentTime; + audio.play(); + }); + video.addEventListener('pause', () => { + audio.pause(); + }); + video.addEventListener('seeked', () => { + audio.currentTime = video.currentTime; + }); + video.addEventListener('ratechange', () => { + audio.currentTime = video.currentTime; + audio.playbackRate = video.playbackRate; + }); + video.addEventListener('waiting', () => { + audio.currentTime = video.currentTime; + audio.pause(); + }); + audio.addEventListener('canplay', () => { + if (audio.currentTime < .1) + video.currentTime = 0; + }, { once: true }); + }, + setupAudioSlider(video, audio) { + const container = document.createElement('span'); + // \u00A0 is non breaking space + container.appendChild(document.createTextNode('🔊︎\u00A0')); + const control = document.createElement('input'); + control.type = 'range'; + control.max = '1'; + control.step = '0.01'; + control.valueAsNumber = audio.volume; + control.addEventListener('input', () => { + audio.volume = control.valueAsNumber; + }); + container.appendChild(control); + const downloadLink = document.createElement('a'); + downloadLink.href = audio.src; + downloadLink.download = ''; + downloadLink.target = '_blank'; + downloadLink.textContent = '\u00A0📥︎'; + container.appendChild(downloadLink); + return container; + }, + }; - this.EAI = $$1.el('a', { - className: 'expand-all-shortcut', - textContent: '➕︎', - title: 'Expand All Images', - href: 'javascript:;' - } - ); - - $$1.on(this.EAI, 'click', this.cb.toggleAll); - Header$1.addShortcut('expand-all', this.EAI, 520); - $$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos); - this.videoControls = $$1.el('span', {className: 'video-controls'}); - $$1.extend(this.videoControls, {innerHTML: " contract"}); - - return Callbacks.Post.push({ - name: 'Image Expansion', - cb: this.node - }); - }, - - node() { - if (!this.file || (!this.file.isImage && !this.file.isVideo)) { return; } - $$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle); - - if (this.isClone) { - if (this.file.isExpanding) { - // If we clone a post where the image is still loading, - // make it loading in the clone too. - ImageExpand.contract(this); - return ImageExpand.expand(this); - - } else if (this.file.isExpanded && this.file.isVideo) { - Volume.setup(this.file.fullImage); - ImageExpand.setupVideoCB(this); - return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls); - } - - } else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote && - (Conf['Expand spoilers'] || !this.file.isSpoiler) && - (Conf['Expand videos'] || !this.file.isVideo)) { - return ImageExpand.expand(this); - } - }, - - cb: { - toggle(e) { - if ($$1.modifiedClick(e)) { return; } - const post = Get$1.postFromNode(this); - const {file} = post; - if (file.isExpanded && ImageCommon.onControls(e)) { return; } - e.preventDefault(); - if (!Conf['Autoplay'] && file.fullImage?.paused) { - return file.fullImage.play(); - } else { - return ImageExpand.toggle(post); - } - }, - - toggleAll() { - let func; - $$1.event('CloseMenu'); - const threadRoot = Nav.getThread(); - const toggle = function(post) { - const {file} = post; - if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) { return; } - if (ImageExpand.on && - ((!Conf['Expand spoilers'] && file.isSpoiler) || - (!Conf['Expand videos'] && file.isVideo) || - (Conf['Expand from here'] && (Header$1.getTopOf(file.thumb) < 0)) || - (Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) { - return; - } - return $$1.queueTask(func, post); - }; - - if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) { - ImageExpand.EAI.className = 'contract-all-shortcut'; - ImageExpand.EAI.title = 'Contract All Images'; - ImageExpand.EAI.textContent = '➖︎'; - func = ImageExpand.expand; - } else { - ImageExpand.EAI.className = 'expand-all-shortcut'; - ImageExpand.EAI.title = 'Expand All Images'; - ImageExpand.EAI.textContent = '➕︎'; - func = ImageExpand.contract; - } - - return g.posts.forEach(function(post) { - for (post of [post, ...post.clones]) { toggle(post); } - }); - }, - - playVideos() { - return g.posts.forEach(function(post) { - for (post of [post, ...post.clones]) { - var {file} = post; - if (!file || !file.isVideo || !file.isExpanded) { continue; } - - var video = file.fullImage; - var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video); - if (visible && file.wasPlaying) { - delete file.wasPlaying; - video.play(); - } else if (!visible && !video.paused) { - file.wasPlaying = true; - video.pause(); - } - } - }); - }, - - setFitness() { - return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-')); - } - }, - - toggle(post) { - if (!post.file.isExpanding && !post.file.isExpanded) { - post.file.scrollIntoView = Conf['Scroll into view']; - ImageExpand.expand(post); - return; - } - - ImageExpand.contract(post); - - if (Conf['Advance on contract']) { - let next = post.nodes.root; - while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) { - if (!$$1('.stub', next) && (next.offsetHeight !== 0)) { break; } - } - if (next) { - return Header$1.scrollTo(next); - } - } - }, - - contract(post) { - let bottom, el, oldHeight, scrollY; - const {file} = post; - - if (el = file.fullImage) { - const top = Header$1.getTopOf(el); - bottom = top + el.getBoundingClientRect().height; - oldHeight = d$1.body.clientHeight; - ({scrollY} = window); - } - - $$1.rmClass(post.nodes.root, 'expanded-image'); - $$1.rmClass(file.thumb, 'expanding'); - $$1.rm(file.videoControls); - file.thumbLink.href = file.url; - file.thumbLink.target = '_blank'; - for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) { - delete file[x]; - } - - if (!el) { return; } - - if (doc$1.contains(el)) { - if (bottom <= 0) { - // For images entirely above us, scroll to remain in place. - window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); - } else { - // For images not above us that would be moved above us, scroll to the thumbnail. - Header$1.scrollToIfNeeded(post.nodes.root); - } - if (window.scrollX > 0) { - // If we have scrolled right viewing an expanded image, return to the left. - window.scrollBy(-window.scrollX, 0); - } - } - - $$1.off(el, 'error', ImageExpand.error); - ImageCommon.pushCache(el); - if (file.isVideo) { - ImageCommon.pause(el); - for (var eventName in ImageExpand.videoCB) { - var cb = ImageExpand.videoCB[eventName]; - $$1.off(el, eventName, cb); - } - } - if (Conf['Restart when Opened']) { ImageCommon.rewind(file.thumb); } - delete file.fullImage; - return $$1.queueTask(function() { - // XXX Work around Chrome/Chromium not firing mouseover on the thumbnail. - if (file.isExpanding || file.isExpanded) { return; } - $$1.rmClass(el, 'full-image'); - if (el.id) { return; } - return $$1.rm(el); - }); - }, - - expand(post, src) { - // Do not expand images of hidden/filtered replies, or already expanded pictures. - let el; - const {file} = post; - const {thumb, thumbLink, isVideo} = file; - if (post.isHidden || file.isExpanding || file.isExpanded) { return; } - - $$1.addClass(thumb, 'expanding'); - file.isExpanding = true; - - if (file.fullImage) { - el = file.fullImage; - } else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) { - el = (file.fullImage = ImageCommon.popCache()); - $$1.on(el, 'error', ImageExpand.error); - if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { ImageCommon.rewind(el); } - el.removeAttribute('id'); - } else { - el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img'))); - el.dataset.fileID = `${post.fullID}.${file.index}`; - $$1.on(el, 'error', ImageExpand.error); - el.src = src || file.url; - } - - el.className = 'full-image'; - $$1.after(thumb, el); - - if (isVideo) { - // add contract link to file info - if (!file.videoControls) { - file.videoControls = ImageExpand.videoControls.cloneNode(true); - $$1.add(file.text, file.videoControls); - } - - // disable link to file so native controls can work - thumbLink.removeAttribute('href'); - thumbLink.removeAttribute('target'); - - el.loop = true; - Volume.setup(el); - ImageExpand.setupVideoCB(post); - } - - if (!isVideo) { - return $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post)); - } else if (el.readyState >= el.HAVE_METADATA) { - return ImageExpand.completeExpand(post); - } else { - return $$1.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post)); - } - }, - - completeExpand(post) { - const {file} = post; - if (!file.isExpanding) { return; } // contracted before the image loaded - - const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; - const oldHeight = d$1.body.clientHeight; - const {scrollY} = window; - - $$1.addClass(post.nodes.root, 'expanded-image'); - $$1.rmClass(file.thumb, 'expanding'); - file.isExpanded = true; - delete file.isExpanding; - - // Scroll to keep our place in the thread when images are expanded above us. - if (doc$1.contains(post.nodes.root) && (bottom <= 0)) { - window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); - } - - // Scroll to display full image. - if (file.scrollIntoView) { - delete file.scrollIntoView; - const imageBottom = Math.min(doc$1.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header$1.getBottomOf(file.fullImage)); - if (imageBottom < 0) { - window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage))); - } - } - - if (file.isVideo) { - return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']); - } - }, - - setupVideo(post, playing, controls) { - const {fullImage} = post.file; - if (!playing) { - fullImage.controls = controls; - return; - } - fullImage.controls = false; - $$1.asap((() => doc$1.contains(fullImage)), function() { - if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) { - return fullImage.play(); - } else { - return post.file.wasPlaying = true; - } - }); - if (controls) { - return ImageCommon.addControls(fullImage); - } - }, - - videoCB: (function() { - // dragging to the left contracts the video - let mousedown = false; - return { - mouseover() { return mousedown = false; }, - mousedown(e) { if (e.button === 0) { return mousedown = true; } }, - mouseup(e) { if (e.button === 0) { return mousedown = false; } }, - mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) { return ImageExpand.toggle(Get$1.postFromNode(this)); } } - }; - })(), - - setupVideoCB(post) { - for (var eventName in ImageExpand.videoCB) { - var cb = ImageExpand.videoCB[eventName]; - $$1.on(post.file.fullImage, eventName, cb); - } - if (post.file.videoControls) { - return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post)); - } - }, - - error() { - const post = Get$1.postFromNode(this); - $$1.rm(this); - delete post.file.fullImage; - // Images can error: - // - before the image started loading. - // - after the image started loading. - // Don't try to re-expand if it was already contracted. - if (!post.file.isExpanding && !post.file.isExpanded) { return; } - if (ImageCommon.decodeError(this, post.file)) { - return ImageExpand.contract(post); - } - // Don't autoretry images from the archive. - if (ImageCommon.isFromArchive(this)) { - return ImageExpand.contract(post); - } - return ImageCommon.error(this, post, post.file, 10 * SECOND, function(URL) { - if (post.file.isExpanding || post.file.isExpanded) { - ImageExpand.contract(post); - if (URL) { return ImageExpand.expand(post, URL); } - } - }); - }, - - menu: { - init() { - if (!ImageExpand.enabled) { return; } - - const el = $$1.el('span', { - textContent: 'Image Expansion', - className: 'image-expansion-link' - } - ); - - const {createSubEntry} = ImageExpand.menu; - const subEntries = []; - for (var name in Config.imageExpansion) { - var conf = Config.imageExpansion[name]; - subEntries.push(createSubEntry(name, conf[1])); - } - - return Header$1.menu.addEntry({ - el, - order: 105, - subEntries - }); - }, - - createSubEntry(name, desc) { - const label = UI.checkbox(name, name); - label.title = desc; - const input = label.firstElementChild; - if (['Fit width', 'Fit height'].includes(name)) { - $$1.on(input, 'change', ImageExpand.cb.setFitness); - } - $$1.event('change', null, input); - $$1.on(input, 'change', $$1.cb.checked); - return {el: label}; - } - } + /* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ + var ImageExpand = { + init() { + if (!(this.enabled = Conf['Image Expansion'] && ['index', 'thread'].includes(g.VIEW))) { + return; + } + this.EAI = $$1.el('a', { + className: 'expand-all-shortcut', + textContent: '➕︎', + title: 'Expand All Images', + href: 'javascript:;' + }); + $$1.on(this.EAI, 'click', this.cb.toggleAll); + Header$1.addShortcut('expand-all', this.EAI, 520); + $$1.on(d$1, 'scroll visibilitychange', this.cb.playVideos); + this.videoControls = $$1.el('span', { className: 'video-controls' }); + $$1.extend(this.videoControls, { innerHTML: " contract" }); + return Callbacks.Post.push({ + name: 'Image Expansion', + cb: this.node + }); + }, + node() { + if (!this.file || (!this.file.isImage && !this.file.isVideo)) { + return; + } + $$1.on(this.file.thumbLink, 'click', ImageExpand.cb.toggle); + if (this.isClone) { + if (this.file.isExpanding) { + // If we clone a post where the image is still loading, + // make it loading in the clone too. + ImageExpand.contract(this); + return ImageExpand.expand(this); + } + else if (this.file.isExpanded && this.file.isVideo) { + Volume.setup(this.file.fullImage); + ImageExpand.setupVideoCB(this); + return ImageExpand.setupVideo(this, !this.origin.file.fullImage?.paused || this.origin.file.wasPlaying, this.file.fullImage.controls); + } + } + else if (ImageExpand.on && !this.isHidden && !this.isFetchedQuote && + (Conf['Expand spoilers'] || !this.file.isSpoiler) && + (Conf['Expand videos'] || !this.file.isVideo)) { + return ImageExpand.expand(this); + } + }, + cb: { + toggle(e) { + if ($$1.modifiedClick(e)) { + return; + } + const post = Get$1.postFromNode(this); + const { file } = post; + if (file.isExpanded && ImageCommon.onControls(e)) { + return; + } + e.preventDefault(); + if (!Conf['Autoplay'] && file.fullImage?.paused) { + return file.fullImage.play(); + } + else { + return ImageExpand.toggle(post); + } + }, + toggleAll() { + let func; + $$1.event('CloseMenu'); + const threadRoot = Nav.getThread(); + const toggle = function (post) { + const { file } = post; + if (!file || (!file.isImage && !file.isVideo) || !doc$1.contains(post.nodes.root)) { + return; + } + if (ImageExpand.on && + ((!Conf['Expand spoilers'] && file.isSpoiler) || + (!Conf['Expand videos'] && file.isVideo) || + (Conf['Expand from here'] && (Header$1.getTopOf(file.thumb) < 0)) || + (Conf['Expand thread only'] && (g.VIEW === 'index') && !threadRoot?.contains(file.thumb)))) { + return; + } + return $$1.queueTask(func, post); + }; + if (ImageExpand.on = $$1.hasClass(ImageExpand.EAI, 'expand-all-shortcut')) { + ImageExpand.EAI.className = 'contract-all-shortcut'; + ImageExpand.EAI.title = 'Contract All Images'; + ImageExpand.EAI.textContent = '➖︎'; + func = ImageExpand.expand; + } + else { + ImageExpand.EAI.className = 'expand-all-shortcut'; + ImageExpand.EAI.title = 'Expand All Images'; + ImageExpand.EAI.textContent = '➕︎'; + func = ImageExpand.contract; + } + return g.posts.forEach(function (post) { + for (post of [post, ...post.clones]) { + toggle(post); + } + }); + }, + playVideos() { + return g.posts.forEach(function (post) { + for (post of [post, ...post.clones]) { + var { file } = post; + if (!file || !file.isVideo || !file.isExpanded) { + continue; + } + var video = file.fullImage; + var visible = ($$1.hasAudio(video) && !video.muted) || Header$1.isNodeVisible(video); + if (visible && file.wasPlaying) { + delete file.wasPlaying; + video.play(); + } + else if (!visible && !video.paused) { + file.wasPlaying = true; + video.pause(); + } + } + }); + }, + setFitness() { + return $$1[this.checked ? 'addClass' : 'rmClass'](doc$1, this.name.toLowerCase().replace(/\s+/g, '-')); + } + }, + toggle(post) { + if (!post.file.isExpanding && !post.file.isExpanded) { + post.file.scrollIntoView = Conf['Scroll into view']; + ImageExpand.expand(post); + return; + } + ImageExpand.contract(post); + if (Conf['Advance on contract']) { + let next = post.nodes.root; + while ((next = $$1.x("following::div[contains(@class,'postContainer')][1]", next))) { + if (!$$1('.stub', next) && (next.offsetHeight !== 0)) { + break; + } + } + if (next) { + return Header$1.scrollTo(next); + } + } + }, + contract(post) { + let bottom, el, oldHeight, scrollY; + const { file } = post; + if (el = file.fullImage) { + const top = Header$1.getTopOf(el); + bottom = top + el.getBoundingClientRect().height; + oldHeight = d$1.body.clientHeight; + ({ scrollY } = window); + } + $$1.rmClass(post.nodes.root, 'expanded-image'); + $$1.rmClass(file.thumb, 'expanding'); + $$1.rm(file.videoControls); + file.thumbLink.href = file.url; + file.thumbLink.target = '_blank'; + for (var x of ['isExpanding', 'isExpanded', 'videoControls', 'wasPlaying', 'scrollIntoView']) { + delete file[x]; + } + if (!el) { + return; + } + if (doc$1.contains(el)) { + if (bottom <= 0) { + // For images entirely above us, scroll to remain in place. + window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); + } + else { + // For images not above us that would be moved above us, scroll to the thumbnail. + Header$1.scrollToIfNeeded(post.nodes.root); + } + if (window.scrollX > 0) { + // If we have scrolled right viewing an expanded image, return to the left. + window.scrollBy(-window.scrollX, 0); + } + } + $$1.off(el, 'error', ImageExpand.error); + ImageCommon.pushCache(el); + if (file.isVideo) { + ImageCommon.pause(el); + for (var eventName in ImageExpand.videoCB) { + var cb = ImageExpand.videoCB[eventName]; + $$1.off(el, eventName, cb); + } + } + if (Conf['Restart when Opened']) { + ImageCommon.rewind(file.thumb); + } + delete file.fullImage; + $$1.queueTask(function () { + // XXX Work around Chrome/Chromium not firing mouseover on the thumbnail. + if (file.isExpanding || file.isExpanded) { + return; + } + $$1.rmClass(el, 'full-image'); + if (el.id) { + return; + } + return $$1.rm(el); + }); + if (file.audio) { + file.audio.remove(); + delete file.audio; + if (file.audioSlider) { + file.audioSlider.remove(); + delete file.audioSlider; + } + } + }, + expand(post, src) { + const { file } = post; + const { thumb, thumbLink, isVideo } = file; + // Do not expand images of hidden/filtered replies, or already expanded pictures. + if (post.isHidden || file.isExpanding || file.isExpanded) { + return; + } + let el; + $$1.addClass(thumb, 'expanding'); + file.isExpanding = true; + if (file.fullImage) { + el = file.fullImage; + } + else if (ImageCommon.cache?.dataset.fileID === `${post.fullID}.${file.index}`) { + el = (file.fullImage = ImageCommon.popCache()); + $$1.on(el, 'error', ImageExpand.error); + if (Conf['Restart when Opened'] && (el.id !== 'ihover')) { + ImageCommon.rewind(el); + } + el.removeAttribute('id'); + } + else { + el = (file.fullImage = $$1.el((isVideo ? 'video' : 'img'))); + el.dataset.fileID = `${post.fullID}.${file.index}`; + $$1.on(el, 'error', ImageExpand.error); + el.src = src || file.url; + } + el.className = 'full-image'; + $$1.after(thumb, el); + if (isVideo) { + // add contract link to file info + if (!file.videoControls) { + file.videoControls = ImageExpand.videoControls.cloneNode(true); + $$1.add(file.text, file.videoControls); + } + // disable link to file so native controls can work + thumbLink.removeAttribute('href'); + thumbLink.removeAttribute('target'); + el.loop = true; + Volume.setup(el); + ImageExpand.setupVideoCB(post); + } + if (!isVideo) { + $$1.asap((() => el.naturalHeight), () => ImageExpand.completeExpand(post)); + } + else if (el.readyState >= el.HAVE_METADATA) { + ImageExpand.completeExpand(post); + } + else { + $$1.on(el, 'loadedmetadata', () => ImageExpand.completeExpand(post)); + } + if (Conf['Enable sound posts'] && Conf['Allow Sound']) { + const soundUrlMatch = file.name.match(/\[sound=([^\]]+)]/); + if (soundUrlMatch) { + let src = decodeURIComponent(soundUrlMatch[1]); + if (!src.startsWith('http')) + src = `https://${src}`; + const audioEl = $$1.el('audio', { src }); + Volume.setup(audioEl); + if (isVideo) { + Audio.setupSync(el, audioEl); + if (Conf['Show Controls']) { + file.audioSlider = Audio.setupAudioSlider(el, audioEl); + $$1.after(el.parentElement, file.audioSlider); + } + } + else { + audioEl.controls = Conf['Show Controls']; + audioEl.autoplay = Conf['Autoplay']; + } + $$1.after(el, audioEl); + file.audio = audioEl; + } + } + }, + completeExpand(post) { + const { file } = post; + if (!file.isExpanding) { + return; + } // contracted before the image loaded + const bottom = Header$1.getTopOf(file.thumb) + file.thumb.getBoundingClientRect().height; + const oldHeight = d$1.body.clientHeight; + const { scrollY } = window; + $$1.addClass(post.nodes.root, 'expanded-image'); + $$1.rmClass(file.thumb, 'expanding'); + file.isExpanded = true; + delete file.isExpanding; + // Scroll to keep our place in the thread when images are expanded above us. + if (doc$1.contains(post.nodes.root) && (bottom <= 0)) { + window.scrollBy(0, ((scrollY - window.scrollY) + d$1.body.clientHeight) - oldHeight); + } + // Scroll to display full image. + if (file.scrollIntoView) { + delete file.scrollIntoView; + const imageBottom = Math.min(doc$1.clientHeight - file.fullImage.getBoundingClientRect().bottom - 25, Header$1.getBottomOf(file.fullImage)); + if (imageBottom < 0) { + window.scrollBy(0, Math.min(-imageBottom, Header$1.getTopOf(file.fullImage))); + } + } + if (file.isVideo) { + return ImageExpand.setupVideo(post, Conf['Autoplay'], Conf['Show Controls']); + } + }, + setupVideo(post, playing, controls) { + const { fullImage } = post.file; + if (!playing) { + fullImage.controls = controls; + return; + } + fullImage.controls = false; + $$1.asap((() => doc$1.contains(fullImage)), function () { + if (!d$1.hidden && Header$1.isNodeVisible(fullImage)) { + return fullImage.play(); + } + else { + return post.file.wasPlaying = true; + } + }); + if (controls) { + return ImageCommon.addControls(fullImage); + } + }, + videoCB: (function () { + // dragging to the left contracts the video + let mousedown = false; + return { + mouseover() { return mousedown = false; }, + mousedown(e) { if (e.button === 0) { + return mousedown = true; + } }, + mouseup(e) { if (e.button === 0) { + return mousedown = false; + } }, + mouseout(e) { if (((e.buttons & 1) || mousedown) && (e.clientX <= this.getBoundingClientRect().left)) { + return ImageExpand.toggle(Get$1.postFromNode(this)); + } } + }; + })(), + setupVideoCB(post) { + for (var eventName in ImageExpand.videoCB) { + var cb = ImageExpand.videoCB[eventName]; + $$1.on(post.file.fullImage, eventName, cb); + } + if (post.file.videoControls) { + return $$1.on(post.file.videoControls.firstElementChild, 'click', () => ImageExpand.toggle(post)); + } + }, + error() { + const post = Get$1.postFromNode(this); + $$1.rm(this); + delete post.file.fullImage; + // Images can error: + // - before the image started loading. + // - after the image started loading. + // Don't try to re-expand if it was already contracted. + if (!post.file.isExpanding && !post.file.isExpanded) { + return; + } + if (ImageCommon.decodeError(this, post.file)) { + return ImageExpand.contract(post); + } + // Don't autoretry images from the archive. + if (ImageCommon.isFromArchive(this)) { + return ImageExpand.contract(post); + } + return ImageCommon.error(this, post, post.file, 10 * SECOND, function (URL) { + if (post.file.isExpanding || post.file.isExpanded) { + ImageExpand.contract(post); + if (URL) { + return ImageExpand.expand(post, URL); + } + } + }); + }, + menu: { + init() { + if (!ImageExpand.enabled) { + return; + } + const el = $$1.el('span', { + textContent: 'Image Expansion', + className: 'image-expansion-link' + }); + const { createSubEntry } = ImageExpand.menu; + const subEntries = []; + for (var name in Config.imageExpansion) { + var conf = Config.imageExpansion[name]; + subEntries.push(createSubEntry(name, conf[1])); + } + return Header$1.menu.addEntry({ + el, + order: 105, + subEntries + }); + }, + createSubEntry(name, desc) { + const label = UI.checkbox(name, name); + label.title = desc; + const input = label.firstElementChild; + if (['Fit width', 'Fit height'].includes(name)) { + $$1.on(input, 'change', ImageExpand.cb.setFitness); + } + $$1.event('change', null, input); + $$1.on(input, 'change', $$1.cb.checked); + return { el: label }; + } + } }; class Post { @@ -11065,6 +11154,10 @@ textarea.copy-text-element { } /* File */ +.expanded-image > .post > .file > .fileThumb { + display: flex; + flex-direction: column; +} .fileText-original, .fnswitch:hover > .fntrunc, .fnswitch:not(:hover) > .fnfull, @@ -11079,6 +11172,11 @@ textarea.copy-text-element { .expanded-image > .post > .file > .fileThumb > .full-image { display: inline; } +.expanded-image > .post > .file > .fileThumb > audio { + height: 30px; + width: 100%; + min-width: 300px; +} .expanded-image { clear: left; } @@ -21341,7 +21439,7 @@ vp-replace } else { $.open = url => window.open(url, '_blank'); - } + } $.debounce = function(wait, fn) { let lastCall = 0; @@ -21432,7 +21530,9 @@ vp-replace : value; - $.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount; + $.hasAudio = video => + video.mozHasAudio || !!video.webkitAudioDecodedByteCount || + video.nextElementSibling?.tagName === 'AUDIO'; // sound posts $.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114); diff --git a/builds/4chan-XT-noupdate.user.min.js b/builds/4chan-XT-noupdate.user.min.js index 724bdaec2..3ddf455d5 100644 --- a/builds/4chan-XT-noupdate.user.min.js +++ b/builds/4chan-XT-noupdate.user.min.js @@ -188,20 +188,20 @@ includes_only:["*://boards.4chan.org/*","*://sys.4chan.org/*","*://www.4chan.org/*","*://boards.4channel.org/*","*://sys.4channel.org/*","*://www.4channel.org/*","*://i.4cdn.org/*","*://is.4chan.org/*","*://is2.4chan.org/*","*://is.4channel.org/*","*://is2.4channel.org/*"],matches_only:["*://*.4chan.org/*","*://*.4channel.org/*","*://*.4cdn.org/*"], matches:["https://erischan.org/*","https://www.erischan.org/*","https://fufufu.moe/*","https://gnfos.com/*","https://himasugi.blog/*","https://www.himasugi.blog/*","https://kakashinenpo.com/*","https://www.kakashinenpo.com/*","https://kissu.moe/*","https://www.kissu.moe/*","https://lainchan.org/*","https://www.lainchan.org/*","https://merorin.com/*","https://ota-ch.com/*","https://www.ota-ch.com/*","https://ponyville.us/*","https://www.ponyville.us/*","https://smuglo.li/*","https://notso.smuglo.li/*","https://smugloli.net/*","https://smug.nepu.moe/*","https://sportschan.org/*","https://www.sportschan.org/*","https://sushigirl.us/*","https://www.sushigirl.us/*","https://tvch.moe/*"],matches_extra:[],exclude_matches:["*://www.4chan.org/advertise","*://www.4chan.org/advertise?*","*://www.4chan.org/donate","*://www.4chan.org/donate?*","*://www.4channel.org/advertise","*://www.4channel.org/advertise?*","*://www.4channel.org/donate","*://www.4channel.org/donate?*"], grants:["GM_getValue","GM_setValue","GM_deleteValue","GM_listValues","GM_addValueChangeListener","GM_openInTab","GM_xmlhttpRequest","GM.getValue","GM.setValue","GM.deleteValue","GM.listValues","GM.openInTab","GM.xmlHttpRequest"],min:{chrome:"80",firefox:"74",greasemonkey:"1.14"}};const t=Object.create(null),n={VERSION:"XT 2.0.0",NAMESPACE:e.name,sites:Object.create(null),boards:Object.create(null)},o=function(){const e={"&":"&","'":"'",'"':""","<":"<",">":">"},t=/[&"'<>]/g,n=function(t){return e[t]},o=function(e){return e.toString().replace(t,n)};return o.cat=function(e){let t="";for(let n=0;n