diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 1bb01d7a2..7d494f7dc 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -115,7 +115,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -10337,8 +10337,8 @@ Gallery = { init: function() { - var el; - if (g.BOARD === 'f' || !Conf['Gallery']) { + var el, _ref; + if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Gallery']) || g.BOARD === 'f') { return; } el = $.el('a', { @@ -10360,7 +10360,7 @@ return; } if (Gallery.nodes) { - Gallery.generateThumb($('.file', this.nodes.root)); + Gallery.generateThumb(this); Gallery.nodes.total.textContent = Gallery.images.length; } if (!Conf['Image Expansion']) { @@ -10368,14 +10368,30 @@ } }, build: function(image) { - var cb, createSubEntry, dialog, el, file, i, key, menuButton, name, nodes, value, _i, _len, _ref, _ref1; + var candidate, cb, dialog, entry, file, key, menuButton, nodes, post, thumb, value, _i, _j, _len, _len1, _ref, _ref1, _ref2; + if (Conf['Fullscreen Gallery']) { + $.one(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', function() { + return $.on(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', cb.close); + }); + if (typeof doc.mozRequestFullScreen === "function") { + doc.mozRequestFullScreen(); + } + if (typeof doc.webkitRequestFullScreen === "function") { + doc.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); + } + } Gallery.images = []; nodes = Gallery.nodes = {}; + Gallery.fullIDs = {}; + Gallery.slideshow = false; nodes.el = dialog = $.el('div', { - id: 'a-gallery', - innerHTML: "
\n" + id: 'a-gallery' + }); + $.extend(dialog, { + innerHTML: "\r" }); _ref = { + buttons: '.gal-buttons', frame: '.gal-image', name: '.gal-name', count: '.count', @@ -10389,58 +10405,71 @@ nodes[key] = $(value, dialog); } menuButton = $('.menu-button', dialog); - nodes.menu = new UI.Menu(); + nodes.menu = new UI.Menu('gallery'); cb = Gallery.cb; $.on(nodes.frame, 'click', cb.blank); - $.on(nodes.next, 'click', cb.advance); + $.on(nodes.next, 'click', cb.click); $.on($('.gal-prev', dialog), 'click', cb.prev); $.on($('.gal-next', dialog), 'click', cb.next); + $.on($('.gal-start', dialog), 'click', cb.start); + $.on($('.gal-stop', dialog), 'click', cb.stop); $.on($('.gal-close', dialog), 'click', cb.close); $.on(menuButton, 'click', function(e) { return nodes.menu.toggle(e, this, g); }); - createSubEntry = Gallery.menu.createSubEntry; - for (name in Config.gallery) { - el = createSubEntry(name).el; - nodes.menu.addEntry({ - el: el, - order: 0 - }); + _ref1 = Gallery.menu.createSubEntries(); + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + entry = _ref1[_i]; + entry.order = 0; + nodes.menu.addEntry(entry); } $.on(d, 'keydown', cb.keybinds); - $.off(d, 'keydown', Keybinds.keydown); - _ref1 = $$('.post .file'); - for (i = _i = 0, _len = _ref1.length; _i < _len; i = ++_i) { - file = _ref1[i]; - if (!$('.fileDeletedRes, .fileDeleted', file)) { - Gallery.generateThumb(file); + if (Conf['Keybinds']) { + $.off(d, 'keydown', Keybinds.keydown); + } + _ref2 = $$('.post .file'); + for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { + file = _ref2[_j]; + post = Get.postFromNode(file); + if (post.file.isDead) { + continue; + } + Gallery.generateThumb(post); + if (!image && Gallery.fullIDs[post.fullID]) { + candidate = post.file.thumb.parentNode; + if (Header.getTopOf(candidate) + candidate.getBoundingClientRect().height >= 0) { + image = candidate; + } } } + $.addClass(doc, 'gallery-open'); $.add(d.body, dialog); nodes.thumbs.scrollTop = 0; nodes.current.parentElement.scrollTop = 0; - Gallery.cb.open.call(image ? $("[href*='" + image.pathname + "']", nodes.thumbs) : Gallery.images[0]); - d.body.style.overflow = 'hidden'; - return nodes.total.textContent = i; + if (image) { + thumb = $("[href='" + image.href + "']", nodes.thumbs); + } + thumb || (thumb = Gallery.images[Gallery.images.length - 1]); + if (thumb) { + Gallery.open(thumb); + } + doc.style.overflow = 'hidden'; + return nodes.total.textContent = Gallery.images.length; }, - generateThumb: function(file) { - var post, thumb, thumbImg, title; - post = Get.postFromNode(file); - if (!(post.file && (post.file.isImage || post.file.isVideo || Conf['PDF in Gallery']))) { + generateThumb: function(post) { + var thumb, thumbImg, _ref, _ref1; + if (post.isClone || post.isHidden && !(((_ref = post.file) != null ? _ref.isImage : void 0) || ((_ref1 = post.file) != null ? _ref1.isVideo : void 0) || Conf['PDF in Gallery'])) { return; } - title = ($('.fileText a', file)).textContent; + Gallery.fullIDs[post.fullID] = true; thumb = $.el('a', { className: 'gal-thumb', href: post.file.URL, target: '_blank', - title: title + title: post.file.name }); thumb.dataset.id = Gallery.images.length; thumb.dataset.post = post.fullID; - if (post.file.isVideo) { - thumb.dataset.isVideo = true; - } thumbImg = post.file.thumb.cloneNode(false); thumbImg.style.cssText = ''; $.add(thumb, thumbImg); @@ -10448,6 +10477,110 @@ Gallery.images.push(thumb); return $.add(Gallery.nodes.thumbs, thumb); }, + open: function(thumb) { + var el, elType, file, name, newID, nodes, oldID, post, slideshow, _base, _ref; + nodes = Gallery.nodes; + name = nodes.name; + oldID = +nodes.current.dataset.id; + newID = +thumb.dataset.id; + slideshow = Gallery.slideshow && (newID > oldID || (oldID === Gallery.images.length - 1 && newID === 0)); + if (el = $('.gal-highlight', nodes.thumbs)) { + $.rmClass(el, 'gal-highlight'); + } + $.addClass(thumb, 'gal-highlight'); + elType = /\.webm$/.test(thumb.href) ? 'video' : /\.pdf$/.test(thumb.href) ? 'iframe' : 'image'; + $[elType === 'iframe' ? 'addClass' : 'rmClass'](doc, 'gal-pdf'); + file = $.el(elType, { + title: name.download = name.textContent = thumb.title + }); + $.on(file, 'error', (function(_this) { + return function() { + return Gallery.error(file, thumb); + }; + })(this)); + file.src = name.href = thumb.href; + $.extend(file.dataset, thumb.dataset); + if (!nodes.current.error) { + if (typeof (_base = nodes.current).pause === "function") { + _base.pause(); + } + } + $.replace(nodes.current, file); + if (elType === 'video') { + file.loop = true; + if (Conf['Autoplay']) { + file.play(); + } + if (Conf['Show Controls']) { + ImageCommon.addControls(file); + } + } + nodes.count.textContent = +thumb.dataset.id + 1; + nodes.current = file; + nodes.frame.scrollTop = 0; + nodes.next.focus(); + if (slideshow) { + Gallery.setupTimer(); + } else { + Gallery.cb.stop(); + } + if (Conf['Scroll to Post'] && (post = (_ref = (post = g.posts[file.dataset.post])) != null ? _ref.nodes.root : void 0)) { + Header.scrollTo(post); + } + return nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight / 2 - nodes.thumbs.clientHeight / 2; + }, + error: function(file, thumb) { + var _ref; + if (((_ref = file.error) != null ? _ref.code : void 0) === MediaError.MEDIA_ERR_DECODE) { + return new Notice('error', 'Corrupt or unplayable video', 30); + } + if (file.src.split('/')[2] !== 'i.4cdn.org') { + return; + } + return ImageCommon.error(file, g.posts[file.dataset.post], null, function(URL) { + if (!URL) { + return; + } + thumb.href = URL; + if (Gallery.nodes.current === file) { + return file.src = URL; + } + }); + }, + cleanupTimer: function() { + var current; + clearTimeout(Gallery.timeoutID); + current = Gallery.nodes.current; + $.off(current, 'canplaythrough load', Gallery.startTimer); + return $.off(current, 'ended', Gallery.cb.next); + }, + startTimer: function() { + return Gallery.timeoutID = setTimeout(Gallery.checkTimer, Gallery.delay * $.SECOND); + }, + setupTimer: function() { + var current, isVideo; + Gallery.cleanupTimer(); + current = Gallery.nodes.current; + isVideo = current.nodeName === 'VIDEO'; + if (isVideo) { + current.play(); + } + if ((isVideo ? current.readyState >= 4 : current.complete) || current.nodeName === 'IFRAME') { + return Gallery.startTimer(); + } else { + return $.on(current, (isVideo ? 'canplaythrough' : 'load'), Gallery.startTimer); + } + }, + checkTimer: function() { + var current; + current = Gallery.nodes.current; + if (current.nodeName === 'VIDEO' && !current.paused) { + $.on(current, 'ended', Gallery.cb.next); + return current.loop = false; + } else { + return Gallery.cb.next(); + } + }, cb: { keybinds: function(e) { var cb, key; @@ -10456,7 +10589,7 @@ } cb = (function() { switch (key) { - case 'Esc': + case Conf['Close']: case Conf['Open Gallery']: return Gallery.cb.close; case 'Right': @@ -10466,6 +10599,10 @@ case 'Left': case '': return Gallery.cb.prev; + case Conf['Pause']: + return Gallery.cb.pause; + case Conf['Slideshow']: + return Gallery.cb.toggleSlideshow; } })(); if (!cb) { @@ -10476,110 +10613,34 @@ return cb(); }, open: function(e) { - var el, elType, file, name, nodes, post, rect, top, _base, _ref; if (e) { e.preventDefault(); } - if (!this) { - return; + if (this) { + return Gallery.open(this); } - nodes = Gallery.nodes; - name = nodes.name; - if (el = $('.gal-highlight', nodes.thumbs)) { - $.rmClass(el, 'gal-highlight'); - } - $.addClass(this, 'gal-highlight'); - elType = this.dataset.isVideo ? 'video' : /\.pdf$/.test(this.href) ? 'iframe' : 'img'; - $[elType === 'iframe' ? 'addClass' : 'rmClass'](nodes.el, 'gal-pdf'); - file = $.el(elType, { - src: name.href = this.href, - title: name.download = name.textContent = this.title - }); - $.extend(file.dataset, this.dataset); - if (typeof (_base = nodes.current).pause === "function") { - _base.pause(); - } - $.replace(nodes.current, file); - if (this.dataset.isVideo) { - Video.configure(file); - } - nodes.count.textContent = +this.dataset.id + 1; - nodes.current = file; - nodes.frame.scrollTop = 0; - nodes.next.focus(); - if (Conf['Scroll to Post'] && (post = (_ref = (post = g.posts[file.dataset.post])) != null ? _ref.nodes.root : void 0)) { - Header.scrollTo(post); - } - $.on(file, 'error', function() { - return Gallery.cb.error(file, thumb); - }); - rect = this.getBoundingClientRect(); - top = rect.top; - if (top > 0) { - top += rect.height - doc.clientHeight; - if (top < 0) { - return; - } - } - return nodes.thumbs.scrollTop += top; }, image: function(e) { e.preventDefault(); e.stopPropagation(); return Gallery.build(this); }, - error: function(img, thumb) { - var URL, post, src; - post = Get.postFromLink($.el('a', { - href: img.dataset.post - })); - delete post.file.fullImage; - src = this.src.split('/'); - if (src[2] === 'i.4cdn.org') { - URL = Redirect.to('file', { - boardID: src[3], - filename: src[src.length - 1] - }); - if (URL) { - thumb.href = URL; - if (Gallery.nodes.current !== img) { - return; - } - img.src = URL; - return; - } - if (g.DEAD || post.isDead || post.file.isDead) { - return; - } - } - return $.ajax("//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", { - onload: function() { - var i, postObj, posts; - if (this.status !== 200) { - return; - } - i = 0; - posts = this.response.posts; - while (postObj = posts[i++]) { - if (postObj.no === post.ID) { - break; - } - } - if (!postObj.no) { - return post.kill(); - } - if (postObj.filedeleted) { - return post.kill(true); - } - } - }); - }, prev: function() { return Gallery.cb.open.call(Gallery.images[+Gallery.nodes.current.dataset.id - 1] || Gallery.images[Gallery.images.length - 1]); }, next: function() { return Gallery.cb.open.call(Gallery.images[+Gallery.nodes.current.dataset.id + 1] || Gallery.images[0]); }, + enterKey: function() { + if (Gallery.nodes.current.paused) { + return Gallery.nodes.current.play(); + } else { + return Gallery.cb.next(); + } + }, + click: function() { + return Gallery.cb[Gallery.nodes.current.controls ? 'stop' : 'enterKey'](); + }, toggle: function() { return (Gallery.nodes ? Gallery.cb.close : Gallery.build)(); }, @@ -10588,41 +10649,66 @@ return Gallery.cb.close(); } }, - advance: function() { - if (Gallery.nodes.current.controls) { - return; - } - if (Gallery.nodes.current.paused) { - return Gallery.nodes.current.play(); - } - return Gallery.cb.next(); - }, pause: function() { var current; + Gallery.cb.stop(); current = Gallery.nodes.current; - if (current.nodeType === 'VIDEO') { + if (current.nodeName === 'VIDEO') { return current[current.paused ? 'play' : 'pause'](); } }, + start: function() { + $.addClass(Gallery.nodes.buttons, 'gal-playing'); + Gallery.slideshow = true; + return Gallery.setupTimer(); + }, + stop: function() { + var current; + if (!Gallery.slideshow) { + return; + } + Gallery.cleanupTimer(); + current = Gallery.nodes.current; + current.loop = current.nodeName === 'VIDEO'; + $.rmClass(Gallery.nodes.buttons, 'gal-playing'); + return Gallery.slideshow = false; + }, close: function() { var _base; if (typeof (_base = Gallery.nodes.current).pause === "function") { _base.pause(); } $.rm(Gallery.nodes.el); + $.rmClass(doc, 'gallery-open'); + if (Conf['Fullscreen Gallery']) { + $.off(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', Gallery.cb.close); + if (typeof d.mozCancelFullScreen === "function") { + d.mozCancelFullScreen(); + } + if (typeof d.webkitExitFullscreen === "function") { + d.webkitExitFullscreen(); + } + } delete Gallery.nodes; - d.body.style.overflow = ''; + delete Gallery.fullIDs; + doc.style.overflow = ''; $.off(d, 'keydown', Gallery.cb.keybinds); - return $.on(d, 'keydown', Keybinds.keydown); + if (Conf['Keybinds']) { + $.on(d, 'keydown', Keybinds.keydown); + } + return clearTimeout(Gallery.timeoutID); }, setFitness: function() { return (this.checked ? $.addClass : $.rmClass)(doc, "gal-" + (this.name.toLowerCase().replace(/\s+/g, '-'))); + }, + setDelay: function() { + return Gallery.delay = +this.value; } }, menu: { init: function() { - var createSubEntry, el, name, subEntries; - if (!Conf['Gallery']) { + var createSubEntry, el, name, subEntries, _ref; + if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Gallery'])) { return; } el = $.el('span', { @@ -10655,10 +10741,149 @@ return { el: label }; + }, + createSubEntries: function() { + var delayInput, delayLabel, item, subEntries; + subEntries = (function() { + var _i, _len, _ref, _results; + _ref = ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Scroll to Post']; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + item = _ref[_i]; + _results.push(Gallery.menu.createSubEntry(item)); + } + return _results; + })(); + delayLabel = $.el('label', { + innerHTML: "Slide Delay: " + }); + delayInput = delayLabel.firstElementChild; + delayInput.value = Gallery.delay; + $.on(delayInput, 'change', Gallery.cb.setDelay); + $.on(delayInput, 'change', $.cb.value); + subEntries.push({ + el: delayLabel + }); + return subEntries; } } }; + ImageCommon = { + rewind: function(el) { + if (el.nodeName === 'VIDEO') { + if (el.readyState >= el.HAVE_METADATA) { + return el.currentTime = 0; + } + } else if (/\.gif$/.test(el.src)) { + return $.queueTask(function() { + return el.src = el.src; + }); + } + }, + pushCache: function(el) { + ImageCommon.cache = el; + return $.on(el, 'error', ImageCommon.cacheError); + }, + popCache: function() { + var el; + el = ImageCommon.cache; + $.off(el, 'error', ImageCommon.cacheError); + delete ImageCommon.cache; + return el; + }, + cacheError: function() { + if (ImageCommon.cache === this) { + return delete ImageCommon.cache; + } + }, + decodeError: function(file, post) { + var message, _ref; + if (((_ref = file.error) != null ? _ref.code : void 0) !== MediaError.MEDIA_ERR_DECODE) { + return false; + } + if (!(message = $('.warning', post.file.thumb.parentNode))) { + message = $.el('div', { + className: 'warning' + }); + $.after(post.file.thumb, message); + } + message.textContent = 'Error: Corrupt or unplayable video'; + return true; + }, + error: function(file, post, delay, cb) { + var URL, redirect, src, timeoutID; + src = post.file.URL.split('/'); + URL = Redirect.to('file', { + boardID: post.board.ID, + filename: src[src.length - 1] + }); + if (!(Conf['404 Redirect'] && URL && Redirect.securityCheck(URL))) { + URL = null; + } + if ((post.isDead || post.file.isDead) && file.src.split('/')[2] === 'i.4cdn.org') { + return cb(URL); + } + if (delay != null) { + timeoutID = setTimeout((function() { + return cb(URL); + }), delay); + } + if (post.isDead || post.file.isDead) { + return; + } + redirect = function() { + if (file.src.split('/')[2] === 'i.4cdn.org') { + if (delay != null) { + clearTimeout(timeoutID); + } + return cb(URL); + } + }; + return $.ajax("//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", { + onload: function() { + var postObj, _i, _len, _ref; + if (this.status === 404) { + post.kill(); + } + if (this.status !== 200) { + return redirect(); + } + _ref = this.response.posts; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + postObj = _ref[_i]; + if (postObj.no === post.ID) { + break; + } + } + if (postObj.no !== post.ID) { + post.kill(); + return redirect(); + } else if (postObj.filedeleted) { + post.kill(true); + return redirect(); + } else { + return URL = post.file.URL; + } + } + }); + }, + addControls: function(video) { + var handler; + handler = function() { + var t; + $.off(video, 'mouseover', handler); + t = new Date().getTime(); + return $.asap((function() { + return (typeof chrome !== "undefined" && chrome !== null) || (video.readyState >= 3 && video.currentTime <= Math.max(0.1, video.duration - 0.5)) || new Date().getTime() >= t + 1000; + }), function() { + return video.controls = true; + }); + }; + return $.on(video, 'mouseover', handler); + } + }; + ImageExpand = { init: function() { if (g.VIEW === 'catalog' || !Conf['Image Expansion']) { diff --git a/builds/crx/script.js b/builds/crx/script.js index c00c1d1b6..07b05489c 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -88,7 +88,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, Banner, Board, Build, Callbacks, CatalogLinks, CatalogThread, Clone, Color, Conf, Config, CrossOrigin, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, E, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Flash, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageCommon, ImageExpand, ImageHover, ImageLoader, Index, JSColor, Keybinds, Labels, Linkify, Main, MarkNewIPs, MascotTools, Mascots, Menu, Nav, Navigate, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteInline, QuoteMarkers, QuotePreview, QuoteStrikeThrough, QuoteThreading, Quotify, RandomAccessList, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, SimpleDict, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadStats, ThreadUpdater, ThreadWatcher, Time, TrashQueue, UI, Unread, Video, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, @@ -10372,8 +10372,8 @@ Gallery = { init: function() { - var el; - if (g.BOARD === 'f' || !Conf['Gallery']) { + var el, _ref; + if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Gallery']) || g.BOARD === 'f') { return; } el = $.el('a', { @@ -10395,7 +10395,7 @@ return; } if (Gallery.nodes) { - Gallery.generateThumb($('.file', this.nodes.root)); + Gallery.generateThumb(this); Gallery.nodes.total.textContent = Gallery.images.length; } if (!Conf['Image Expansion']) { @@ -10403,14 +10403,30 @@ } }, build: function(image) { - var cb, createSubEntry, dialog, el, file, i, key, menuButton, name, nodes, value, _i, _len, _ref, _ref1; + var candidate, cb, dialog, entry, file, key, menuButton, nodes, post, thumb, value, _i, _j, _len, _len1, _ref, _ref1, _ref2; + if (Conf['Fullscreen Gallery']) { + $.one(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', function() { + return $.on(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', cb.close); + }); + if (typeof doc.mozRequestFullScreen === "function") { + doc.mozRequestFullScreen(); + } + if (typeof doc.webkitRequestFullScreen === "function") { + doc.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); + } + } Gallery.images = []; nodes = Gallery.nodes = {}; + Gallery.fullIDs = {}; + Gallery.slideshow = false; nodes.el = dialog = $.el('div', { - id: 'a-gallery', - innerHTML: "\n" + id: 'a-gallery' + }); + $.extend(dialog, { + innerHTML: "\r" }); _ref = { + buttons: '.gal-buttons', frame: '.gal-image', name: '.gal-name', count: '.count', @@ -10424,58 +10440,71 @@ nodes[key] = $(value, dialog); } menuButton = $('.menu-button', dialog); - nodes.menu = new UI.Menu(); + nodes.menu = new UI.Menu('gallery'); cb = Gallery.cb; $.on(nodes.frame, 'click', cb.blank); - $.on(nodes.next, 'click', cb.advance); + $.on(nodes.next, 'click', cb.click); $.on($('.gal-prev', dialog), 'click', cb.prev); $.on($('.gal-next', dialog), 'click', cb.next); + $.on($('.gal-start', dialog), 'click', cb.start); + $.on($('.gal-stop', dialog), 'click', cb.stop); $.on($('.gal-close', dialog), 'click', cb.close); $.on(menuButton, 'click', function(e) { return nodes.menu.toggle(e, this, g); }); - createSubEntry = Gallery.menu.createSubEntry; - for (name in Config.gallery) { - el = createSubEntry(name).el; - nodes.menu.addEntry({ - el: el, - order: 0 - }); + _ref1 = Gallery.menu.createSubEntries(); + for (_i = 0, _len = _ref1.length; _i < _len; _i++) { + entry = _ref1[_i]; + entry.order = 0; + nodes.menu.addEntry(entry); } $.on(d, 'keydown', cb.keybinds); - $.off(d, 'keydown', Keybinds.keydown); - _ref1 = $$('.post .file'); - for (i = _i = 0, _len = _ref1.length; _i < _len; i = ++_i) { - file = _ref1[i]; - if (!$('.fileDeletedRes, .fileDeleted', file)) { - Gallery.generateThumb(file); + if (Conf['Keybinds']) { + $.off(d, 'keydown', Keybinds.keydown); + } + _ref2 = $$('.post .file'); + for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { + file = _ref2[_j]; + post = Get.postFromNode(file); + if (post.file.isDead) { + continue; + } + Gallery.generateThumb(post); + if (!image && Gallery.fullIDs[post.fullID]) { + candidate = post.file.thumb.parentNode; + if (Header.getTopOf(candidate) + candidate.getBoundingClientRect().height >= 0) { + image = candidate; + } } } + $.addClass(doc, 'gallery-open'); $.add(d.body, dialog); nodes.thumbs.scrollTop = 0; nodes.current.parentElement.scrollTop = 0; - Gallery.cb.open.call(image ? $("[href*='" + image.pathname + "']", nodes.thumbs) : Gallery.images[0]); - d.body.style.overflow = 'hidden'; - return nodes.total.textContent = i; + if (image) { + thumb = $("[href='" + image.href + "']", nodes.thumbs); + } + thumb || (thumb = Gallery.images[Gallery.images.length - 1]); + if (thumb) { + Gallery.open(thumb); + } + doc.style.overflow = 'hidden'; + return nodes.total.textContent = Gallery.images.length; }, - generateThumb: function(file) { - var post, thumb, thumbImg, title; - post = Get.postFromNode(file); - if (!(post.file && (post.file.isImage || post.file.isVideo || Conf['PDF in Gallery']))) { + generateThumb: function(post) { + var thumb, thumbImg, _ref, _ref1; + if (post.isClone || post.isHidden && !(((_ref = post.file) != null ? _ref.isImage : void 0) || ((_ref1 = post.file) != null ? _ref1.isVideo : void 0) || Conf['PDF in Gallery'])) { return; } - title = ($('.fileText a', file)).textContent; + Gallery.fullIDs[post.fullID] = true; thumb = $.el('a', { className: 'gal-thumb', href: post.file.URL, target: '_blank', - title: title + title: post.file.name }); thumb.dataset.id = Gallery.images.length; thumb.dataset.post = post.fullID; - if (post.file.isVideo) { - thumb.dataset.isVideo = true; - } thumbImg = post.file.thumb.cloneNode(false); thumbImg.style.cssText = ''; $.add(thumb, thumbImg); @@ -10483,6 +10512,110 @@ Gallery.images.push(thumb); return $.add(Gallery.nodes.thumbs, thumb); }, + open: function(thumb) { + var el, elType, file, name, newID, nodes, oldID, post, slideshow, _base, _ref; + nodes = Gallery.nodes; + name = nodes.name; + oldID = +nodes.current.dataset.id; + newID = +thumb.dataset.id; + slideshow = Gallery.slideshow && (newID > oldID || (oldID === Gallery.images.length - 1 && newID === 0)); + if (el = $('.gal-highlight', nodes.thumbs)) { + $.rmClass(el, 'gal-highlight'); + } + $.addClass(thumb, 'gal-highlight'); + elType = /\.webm$/.test(thumb.href) ? 'video' : /\.pdf$/.test(thumb.href) ? 'iframe' : 'image'; + $[elType === 'iframe' ? 'addClass' : 'rmClass'](doc, 'gal-pdf'); + file = $.el(elType, { + title: name.download = name.textContent = thumb.title + }); + $.on(file, 'error', (function(_this) { + return function() { + return Gallery.error(file, thumb); + }; + })(this)); + file.src = name.href = thumb.href; + $.extend(file.dataset, thumb.dataset); + if (!nodes.current.error) { + if (typeof (_base = nodes.current).pause === "function") { + _base.pause(); + } + } + $.replace(nodes.current, file); + if (elType === 'video') { + file.loop = true; + if (Conf['Autoplay']) { + file.play(); + } + if (Conf['Show Controls']) { + ImageCommon.addControls(file); + } + } + nodes.count.textContent = +thumb.dataset.id + 1; + nodes.current = file; + nodes.frame.scrollTop = 0; + nodes.next.focus(); + if (slideshow) { + Gallery.setupTimer(); + } else { + Gallery.cb.stop(); + } + if (Conf['Scroll to Post'] && (post = (_ref = (post = g.posts[file.dataset.post])) != null ? _ref.nodes.root : void 0)) { + Header.scrollTo(post); + } + return nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight / 2 - nodes.thumbs.clientHeight / 2; + }, + error: function(file, thumb) { + var _ref; + if (((_ref = file.error) != null ? _ref.code : void 0) === MediaError.MEDIA_ERR_DECODE) { + return new Notice('error', 'Corrupt or unplayable video', 30); + } + if (file.src.split('/')[2] !== 'i.4cdn.org') { + return; + } + return ImageCommon.error(file, g.posts[file.dataset.post], null, function(URL) { + if (!URL) { + return; + } + thumb.href = URL; + if (Gallery.nodes.current === file) { + return file.src = URL; + } + }); + }, + cleanupTimer: function() { + var current; + clearTimeout(Gallery.timeoutID); + current = Gallery.nodes.current; + $.off(current, 'canplaythrough load', Gallery.startTimer); + return $.off(current, 'ended', Gallery.cb.next); + }, + startTimer: function() { + return Gallery.timeoutID = setTimeout(Gallery.checkTimer, Gallery.delay * $.SECOND); + }, + setupTimer: function() { + var current, isVideo; + Gallery.cleanupTimer(); + current = Gallery.nodes.current; + isVideo = current.nodeName === 'VIDEO'; + if (isVideo) { + current.play(); + } + if ((isVideo ? current.readyState >= 4 : current.complete) || current.nodeName === 'IFRAME') { + return Gallery.startTimer(); + } else { + return $.on(current, (isVideo ? 'canplaythrough' : 'load'), Gallery.startTimer); + } + }, + checkTimer: function() { + var current; + current = Gallery.nodes.current; + if (current.nodeName === 'VIDEO' && !current.paused) { + $.on(current, 'ended', Gallery.cb.next); + return current.loop = false; + } else { + return Gallery.cb.next(); + } + }, cb: { keybinds: function(e) { var cb, key; @@ -10491,7 +10624,7 @@ } cb = (function() { switch (key) { - case 'Esc': + case Conf['Close']: case Conf['Open Gallery']: return Gallery.cb.close; case 'Right': @@ -10501,6 +10634,10 @@ case 'Left': case '': return Gallery.cb.prev; + case Conf['Pause']: + return Gallery.cb.pause; + case Conf['Slideshow']: + return Gallery.cb.toggleSlideshow; } })(); if (!cb) { @@ -10511,110 +10648,34 @@ return cb(); }, open: function(e) { - var el, elType, file, name, nodes, post, rect, top, _base, _ref; if (e) { e.preventDefault(); } - if (!this) { - return; + if (this) { + return Gallery.open(this); } - nodes = Gallery.nodes; - name = nodes.name; - if (el = $('.gal-highlight', nodes.thumbs)) { - $.rmClass(el, 'gal-highlight'); - } - $.addClass(this, 'gal-highlight'); - elType = this.dataset.isVideo ? 'video' : /\.pdf$/.test(this.href) ? 'iframe' : 'img'; - $[elType === 'iframe' ? 'addClass' : 'rmClass'](nodes.el, 'gal-pdf'); - file = $.el(elType, { - src: name.href = this.href, - title: name.download = name.textContent = this.title - }); - $.extend(file.dataset, this.dataset); - if (typeof (_base = nodes.current).pause === "function") { - _base.pause(); - } - $.replace(nodes.current, file); - if (this.dataset.isVideo) { - Video.configure(file); - } - nodes.count.textContent = +this.dataset.id + 1; - nodes.current = file; - nodes.frame.scrollTop = 0; - nodes.next.focus(); - if (Conf['Scroll to Post'] && (post = (_ref = (post = g.posts[file.dataset.post])) != null ? _ref.nodes.root : void 0)) { - Header.scrollTo(post); - } - $.on(file, 'error', function() { - return Gallery.cb.error(file, thumb); - }); - rect = this.getBoundingClientRect(); - top = rect.top; - if (top > 0) { - top += rect.height - doc.clientHeight; - if (top < 0) { - return; - } - } - return nodes.thumbs.scrollTop += top; }, image: function(e) { e.preventDefault(); e.stopPropagation(); return Gallery.build(this); }, - error: function(img, thumb) { - var URL, post, src; - post = Get.postFromLink($.el('a', { - href: img.dataset.post - })); - delete post.file.fullImage; - src = this.src.split('/'); - if (src[2] === 'i.4cdn.org') { - URL = Redirect.to('file', { - boardID: src[3], - filename: src[src.length - 1] - }); - if (URL) { - thumb.href = URL; - if (Gallery.nodes.current !== img) { - return; - } - img.src = URL; - return; - } - if (g.DEAD || post.isDead || post.file.isDead) { - return; - } - } - return $.ajax("//a.4cdn.org/" + post.board + "/thread/" + post.thread + ".json", { - onload: function() { - var i, postObj, posts; - if (this.status !== 200) { - return; - } - i = 0; - posts = this.response.posts; - while (postObj = posts[i++]) { - if (postObj.no === post.ID) { - break; - } - } - if (!postObj.no) { - return post.kill(); - } - if (postObj.filedeleted) { - return post.kill(true); - } - } - }); - }, prev: function() { return Gallery.cb.open.call(Gallery.images[+Gallery.nodes.current.dataset.id - 1] || Gallery.images[Gallery.images.length - 1]); }, next: function() { return Gallery.cb.open.call(Gallery.images[+Gallery.nodes.current.dataset.id + 1] || Gallery.images[0]); }, + enterKey: function() { + if (Gallery.nodes.current.paused) { + return Gallery.nodes.current.play(); + } else { + return Gallery.cb.next(); + } + }, + click: function() { + return Gallery.cb[Gallery.nodes.current.controls ? 'stop' : 'enterKey'](); + }, toggle: function() { return (Gallery.nodes ? Gallery.cb.close : Gallery.build)(); }, @@ -10623,41 +10684,66 @@ return Gallery.cb.close(); } }, - advance: function() { - if (Gallery.nodes.current.controls) { - return; - } - if (Gallery.nodes.current.paused) { - return Gallery.nodes.current.play(); - } - return Gallery.cb.next(); - }, pause: function() { var current; + Gallery.cb.stop(); current = Gallery.nodes.current; - if (current.nodeType === 'VIDEO') { + if (current.nodeName === 'VIDEO') { return current[current.paused ? 'play' : 'pause'](); } }, + start: function() { + $.addClass(Gallery.nodes.buttons, 'gal-playing'); + Gallery.slideshow = true; + return Gallery.setupTimer(); + }, + stop: function() { + var current; + if (!Gallery.slideshow) { + return; + } + Gallery.cleanupTimer(); + current = Gallery.nodes.current; + current.loop = current.nodeName === 'VIDEO'; + $.rmClass(Gallery.nodes.buttons, 'gal-playing'); + return Gallery.slideshow = false; + }, close: function() { var _base; if (typeof (_base = Gallery.nodes.current).pause === "function") { _base.pause(); } $.rm(Gallery.nodes.el); + $.rmClass(doc, 'gallery-open'); + if (Conf['Fullscreen Gallery']) { + $.off(d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', Gallery.cb.close); + if (typeof d.mozCancelFullScreen === "function") { + d.mozCancelFullScreen(); + } + if (typeof d.webkitExitFullscreen === "function") { + d.webkitExitFullscreen(); + } + } delete Gallery.nodes; - d.body.style.overflow = ''; + delete Gallery.fullIDs; + doc.style.overflow = ''; $.off(d, 'keydown', Gallery.cb.keybinds); - return $.on(d, 'keydown', Keybinds.keydown); + if (Conf['Keybinds']) { + $.on(d, 'keydown', Keybinds.keydown); + } + return clearTimeout(Gallery.timeoutID); }, setFitness: function() { return (this.checked ? $.addClass : $.rmClass)(doc, "gal-" + (this.name.toLowerCase().replace(/\s+/g, '-'))); + }, + setDelay: function() { + return Gallery.delay = +this.value; } }, menu: { init: function() { - var createSubEntry, el, name, subEntries; - if (!Conf['Gallery']) { + var createSubEntry, el, name, subEntries, _ref; + if (!(((_ref = g.VIEW) === 'index' || _ref === 'thread') && Conf['Gallery'])) { return; } el = $.el('span', { @@ -10690,10 +10776,136 @@ return { el: label }; + }, + createSubEntries: function() { + var delayInput, delayLabel, item, subEntries; + subEntries = (function() { + var _i, _len, _ref, _results; + _ref = ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Scroll to Post']; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + item = _ref[_i]; + _results.push(Gallery.menu.createSubEntry(item)); + } + return _results; + })(); + delayLabel = $.el('label', { + innerHTML: "Slide Delay: " + }); + delayInput = delayLabel.firstElementChild; + delayInput.value = Gallery.delay; + $.on(delayInput, 'change', Gallery.cb.setDelay); + $.on(delayInput, 'change', $.cb.value); + subEntries.push({ + el: delayLabel + }); + return subEntries; } } }; + ImageCommon = { + rewind: function(el) { + if (el.nodeName === 'VIDEO') { + if (el.readyState >= el.HAVE_METADATA) { + return el.currentTime = 0; + } + } else if (/\.gif$/.test(el.src)) { + return $.queueTask(function() { + return el.src = el.src; + }); + } + }, + pushCache: function(el) { + ImageCommon.cache = el; + return $.on(el, 'error', ImageCommon.cacheError); + }, + popCache: function() { + var el; + el = ImageCommon.cache; + $.off(el, 'error', ImageCommon.cacheError); + delete ImageCommon.cache; + return el; + }, + cacheError: function() { + if (ImageCommon.cache === this) { + return delete ImageCommon.cache; + } + }, + decodeError: function(file, post) { + var message, _ref; + if (((_ref = file.error) != null ? _ref.code : void 0) !== MediaError.MEDIA_ERR_DECODE) { + return false; + } + if (!(message = $('.warning', post.file.thumb.parentNode))) { + message = $.el('div', { + className: 'warning' + }); + $.after(post.file.thumb, message); + } + message.textContent = 'Error: Corrupt or unplayable video'; + return true; + }, + error: function(file, post, delay, cb) { + var URL, redirect, src, timeoutID; + src = post.file.URL.split('/'); + URL = Redirect.to('file', { + boardID: post.board.ID, + filename: src[src.length - 1] + }); + if (!(Conf['404 Redirect'] && URL && Redirect.securityCheck(URL))) { + URL = null; + } + if ((post.isDead || post.file.isDead) && file.src.split('/')[2] === 'i.4cdn.org') { + return cb(URL); + } + if (delay != null) { + timeoutID = setTimeout((function() { + return cb(URL); + }), delay); + } + if (post.isDead || post.file.isDead) { + return; + } + redirect = function() { + if (file.src.split('/')[2] === 'i.4cdn.org') { + if (delay != null) { + clearTimeout(timeoutID); + } + return cb(URL); + } + }; + return $.ajax(post.file.URL, { + onloadend: function() { + if (this.status === 200) { + return URL = post.file.URL; + } else { + if (this.status === 404) { + post.kill(true); + } + return redirect(); + } + } + }, { + type: 'head' + }); + }, + addControls: function(video) { + var handler; + handler = function() { + var t; + $.off(video, 'mouseover', handler); + t = new Date().getTime(); + return $.asap((function() { + return (typeof chrome !== "undefined" && chrome !== null) || (video.readyState >= 3 && video.currentTime <= Math.max(0.1, video.duration - 0.5)) || new Date().getTime() >= t + 1000; + }), function() { + return video.controls = true; + }); + }; + return $.on(video, 'mouseover', handler); + } + }; + ImageExpand = { init: function() { if (g.VIEW === 'catalog' || !Conf['Image Expansion']) { diff --git a/src/General/html/Features/Gallery.html b/src/General/html/Features/Gallery.html new file mode 100644 index 000000000..ed76ff59f --- /dev/null +++ b/src/General/html/Features/Gallery.html @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/src/Images/Gallery.coffee b/src/Images/Gallery.coffee index 503fd2e49..710b66ada 100644 --- a/src/Images/Gallery.coffee +++ b/src/Images/Gallery.coffee @@ -1,6 +1,6 @@ Gallery = init: -> - return if g.BOARD is 'f' or !Conf['Gallery'] + return if not (g.VIEW in ['index', 'thread'] and Conf['Gallery']) or g.BOARD is 'f' el = $.el 'a', href: 'javascript:;' @@ -20,38 +20,30 @@ Gallery = node: -> return unless @file if Gallery.nodes - Gallery.generateThumb $ '.file', @nodes.root + Gallery.generateThumb @ Gallery.nodes.total.textContent = Gallery.images.length unless Conf['Image Expansion'] $.on @file.thumb.parentNode, 'click', Gallery.cb.image build: (image) -> + if Conf['Fullscreen Gallery'] + $.one d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', -> + $.on d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', cb.close + doc.mozRequestFullScreen?() + doc.webkitRequestFullScreen?(Element.ALLOW_KEYBOARD_INPUT) + Gallery.images = [] nodes = Gallery.nodes = {} + Gallery.fullIDs = {} + Gallery.slideshow = false nodes.el = dialog = $.el 'div', id: 'a-gallery' - innerHTML: """ - - - """ + $.extend dialog, <%= importHTML('Features/Gallery') %> nodes[key] = $ value, dialog for key, value of { + buttons: '.gal-buttons' frame: '.gal-image' name: '.gal-name' count: '.count' @@ -62,56 +54,64 @@ Gallery = } menuButton = $ '.menu-button', dialog - nodes.menu = new UI.Menu() + nodes.menu = new UI.Menu 'gallery' {cb} = Gallery $.on nodes.frame, 'click', cb.blank - $.on nodes.next, 'click', cb.advance + $.on nodes.next, 'click', cb.click $.on $('.gal-prev', dialog), 'click', cb.prev $.on $('.gal-next', dialog), 'click', cb.next + $.on $('.gal-start', dialog), 'click', cb.start + $.on $('.gal-stop', dialog), 'click', cb.stop $.on $('.gal-close', dialog), 'click', cb.close $.on menuButton, 'click', (e) -> nodes.menu.toggle e, @, g - {createSubEntry} = Gallery.menu - for name of Config.gallery - {el} = createSubEntry name - - nodes.menu.addEntry - el: el - order: 0 + for entry in Gallery.menu.createSubEntries() + entry.order = 0 + nodes.menu.addEntry entry $.on d, 'keydown', cb.keybinds - $.off d, 'keydown', Keybinds.keydown - Gallery.generateThumb file for file, i in $$ '.post .file' when !$ '.fileDeletedRes, .fileDeleted', file + $.off d, 'keydown', Keybinds.keydown if Conf['Keybinds'] + + for file in $$ '.post .file' + post = Get.postFromNode file + continue if post.file.isDead + Gallery.generateThumb post + # If no image to open is given, pick image we have scrolled to. + if !image and Gallery.fullIDs[post.fullID] + candidate = post.file.thumb.parentNode + if Header.getTopOf(candidate) + candidate.getBoundingClientRect().height >= 0 + image = candidate + $.addClass doc, 'gallery-open' + $.add d.body, dialog nodes.thumbs.scrollTop = 0 nodes.current.parentElement.scrollTop = 0 - Gallery.cb.open.call if image - $ "[href*='#{image.pathname}']", nodes.thumbs - else - Gallery.images[0] + thumb = $ "[href='#{image.href}']", nodes.thumbs if image + thumb or= Gallery.images[Gallery.images.length-1] + Gallery.open thumb if thumb - d.body.style.overflow = 'hidden' - nodes.total.textContent = i + doc.style.overflow = 'hidden' + nodes.total.textContent = Gallery.images.length - generateThumb: (file) -> - post = Get.postFromNode file - return unless post.file and (post.file.isImage or post.file.isVideo or Conf['PDF in Gallery']) - title = ($ '.fileText a', file).textContent + generateThumb: (post) -> + return if post.isClone or post.isHidden and + not (post.file?.isImage or post.file?.isVideo or Conf['PDF in Gallery']) + + Gallery.fullIDs[post.fullID] = true thumb = $.el 'a', className: 'gal-thumb' href: post.file.URL target: '_blank' - title: title + title: post.file.name - thumb.dataset.id = Gallery.images.length - thumb.dataset.post = post.fullID - thumb.dataset.isVideo = true if post.file.isVideo + thumb.dataset.id = Gallery.images.length + thumb.dataset.post = post.fullID thumbImg = post.file.thumb.cloneNode false thumbImg.style.cssText = '' @@ -122,12 +122,95 @@ Gallery = Gallery.images.push thumb $.add Gallery.nodes.thumbs, thumb + open: (thumb) -> + {nodes} = Gallery + {name} = nodes + oldID = +nodes.current.dataset.id + newID = +thumb.dataset.id + slideshow = Gallery.slideshow and (newID > oldID or (oldID is Gallery.images.length-1 and newID is 0)) + + $.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs + $.addClass thumb, 'gal-highlight' + + elType = if /\.webm$/.test(thumb.href) + 'video' + else if /\.pdf$/.test(thumb.href) + 'iframe' + else + 'image' + + $[if elType is 'iframe' then 'addClass' else 'rmClass'] doc, 'gal-pdf' + file = $.el elType, + title: name.download = name.textContent = thumb.title + $.on file, 'error', => + Gallery.error file, thumb + file.src = name.href = thumb.href + + $.extend file.dataset, thumb.dataset + nodes.current.pause?() unless nodes.current.error + $.replace nodes.current, file + if elType is 'video' + file.loop = true + file.play() if Conf['Autoplay'] + ImageCommon.addControls file if Conf['Show Controls'] + nodes.count.textContent = +thumb.dataset.id + 1 + nodes.current = file + nodes.frame.scrollTop = 0 + nodes.next.focus() + if slideshow + Gallery.setupTimer() + else + Gallery.cb.stop() + + # Scroll to post + if Conf['Scroll to Post'] and post = (post = g.posts[file.dataset.post])?.nodes.root + Header.scrollTo post + + # Center selected thumbnail + nodes.thumbs.scrollTop = thumb.offsetTop + thumb.offsetHeight/2 - nodes.thumbs.clientHeight/2 + + error: (file, thumb) -> + if file.error?.code is MediaError.MEDIA_ERR_DECODE + return new Notice 'error', 'Corrupt or unplayable video', 30 + return unless file.src.split('/')[2] is 'i.4cdn.org' + ImageCommon.error file, g.posts[file.dataset.post], null, (URL) -> + return unless URL + thumb.href = URL + file.src = URL if Gallery.nodes.current is file + + cleanupTimer: -> + clearTimeout Gallery.timeoutID + {current} = Gallery.nodes + $.off current, 'canplaythrough load', Gallery.startTimer + $.off current, 'ended', Gallery.cb.next + + startTimer: -> + Gallery.timeoutID = setTimeout Gallery.checkTimer, Gallery.delay * $.SECOND + + setupTimer: -> + Gallery.cleanupTimer() + {current} = Gallery.nodes + isVideo = current.nodeName is 'VIDEO' + current.play() if isVideo + if (if isVideo then current.readyState >= 4 else current.complete) or current.nodeName is 'IFRAME' + Gallery.startTimer() + else + $.on current, (if isVideo then 'canplaythrough' else 'load'), Gallery.startTimer + + checkTimer: -> + {current} = Gallery.nodes + if current.nodeName is 'VIDEO' and !current.paused + $.on current, 'ended', Gallery.cb.next + current.loop = false + else + Gallery.cb.next() + cb: keybinds: (e) -> return unless key = Keybinds.keyCode e cb = switch key - when 'Esc', Conf['Open Gallery'] + when Conf['Close'], Conf['Open Gallery'] Gallery.cb.close when 'Right' Gallery.cb.next @@ -135,6 +218,10 @@ Gallery = Gallery.cb.advance when 'Left', '' Gallery.cb.prev + when Conf['Pause'] + Gallery.cb.pause + when Conf['Slideshow'] + Gallery.cb.toggleSlideshow return unless cb e.stopPropagation() @@ -143,80 +230,13 @@ Gallery = open: (e) -> e.preventDefault() if e - return unless @ - - {nodes} = Gallery - {name} = nodes - - $.rmClass el, 'gal-highlight' if el = $ '.gal-highlight', nodes.thumbs - $.addClass @, 'gal-highlight' - - elType = if @dataset.isVideo then 'video' else if /\.pdf$/.test(@href) then 'iframe' else 'img' - $[if elType is 'iframe' then 'addClass' else 'rmClass'] nodes.el, 'gal-pdf' - - file = $.el elType, - src: name.href = @href - title: name.download = name.textContent = @title - - $.extend file.dataset, @dataset - nodes.current.pause?() - $.replace nodes.current, file - Video.configure file if @dataset.isVideo - nodes.count.textContent = +@dataset.id + 1 - nodes.current = file - nodes.frame.scrollTop = 0 - nodes.next.focus() - - # Scroll to post - if Conf['Scroll to Post'] and post = (post = g.posts[file.dataset.post])?.nodes.root - Header.scrollTo post - - $.on file, 'error', -> - Gallery.cb.error file, thumb - - # Scroll - rect = @getBoundingClientRect() - {top} = rect - if top > 0 - top += rect.height - doc.clientHeight - return if top < 0 - - nodes.thumbs.scrollTop += top + if @ then Gallery.open @ image: (e) -> e.preventDefault() e.stopPropagation() Gallery.build @ - error: (img, thumb) -> - post = Get.postFromLink $.el 'a', href: img.dataset.post - delete post.file.fullImage - - src = @src.split '/' - if src[2] is 'i.4cdn.org' - URL = Redirect.to 'file', - boardID: src[3] - filename: src[src.length - 1] - if URL - thumb.href = URL - return unless Gallery.nodes.current is img - img.src = URL - return - if g.DEAD or post.isDead or post.file.isDead - return - - # XXX CORS for i.4cdn.org WHEN? - $.ajax "//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: -> - return if @status isnt 200 - i = 0 - {posts} = @response - while postObj = posts[i++] - break if postObj.no is post.ID - unless postObj.no - return post.kill() - if postObj.filedeleted - post.kill true - prev: -> Gallery.cb.open.call( Gallery.images[+Gallery.nodes.current.dataset.id - 1] or Gallery.images[Gallery.images.length - 1] @@ -225,33 +245,54 @@ Gallery = Gallery.cb.open.call( Gallery.images[+Gallery.nodes.current.dataset.id + 1] or Gallery.images[0] ) + + enterKey: -> if Gallery.nodes.current.paused then Gallery.nodes.current.play() else Gallery.cb.next() + click: -> Gallery.cb[if Gallery.nodes.current.controls then 'stop' else 'enterKey']() toggle: -> (if Gallery.nodes then Gallery.cb.close else Gallery.build)() blank: (e) -> Gallery.cb.close() if e.target is @ - - advance: -> - if Gallery.nodes.current.controls then return - if Gallery.nodes.current.paused then return Gallery.nodes.current.play() - Gallery.cb.next() pause: -> + Gallery.cb.stop() {current} = Gallery.nodes - current[if current.paused then 'play' else 'pause']() if current.nodeType is 'VIDEO' + current[if current.paused then 'play' else 'pause']() if current.nodeName is 'VIDEO' + + start: -> + $.addClass Gallery.nodes.buttons, 'gal-playing' + Gallery.slideshow = true + Gallery.setupTimer() + + stop: -> + return unless Gallery.slideshow + Gallery.cleanupTimer() + {current} = Gallery.nodes + current.loop = current.nodeName is 'VIDEO' + $.rmClass Gallery.nodes.buttons, 'gal-playing' + Gallery.slideshow = false close: -> Gallery.nodes.current.pause?() $.rm Gallery.nodes.el + $.rmClass doc, 'gallery-open' + if Conf['Fullscreen Gallery'] + $.off d, 'fullscreenchange mozfullscreenchange webkitfullscreenchange', Gallery.cb.close + d.mozCancelFullScreen?() + d.webkitExitFullscreen?() delete Gallery.nodes - d.body.style.overflow = '' + delete Gallery.fullIDs + doc.style.overflow = '' $.off d, 'keydown', Gallery.cb.keybinds - $.on d, 'keydown', Keybinds.keydown + $.on d, 'keydown', Keybinds.keydown if Conf['Keybinds'] + clearTimeout Gallery.timeoutID setFitness: -> (if @checked then $.addClass else $.rmClass) doc, "gal-#{@name.toLowerCase().replace /\s+/g, '-'}" + setDelay: -> Gallery.delay = +@value + menu: init: -> - return if !Conf['Gallery'] + return unless g.VIEW in ['index', 'thread'] and Conf['Gallery'] el = $.el 'span', textContent: 'Gallery' @@ -277,3 +318,15 @@ Gallery = $.event 'change', null, input $.on input, 'change', $.cb.checked el: label + + createSubEntries: -> + subEntries = (Gallery.menu.createSubEntry item for item in ['Hide Thumbnails', 'Fit Width', 'Fit Height', 'Scroll to Post']) + + delayLabel = $.el 'label', <%= html('Slide Delay: ') %> + delayInput = delayLabel.firstElementChild + delayInput.value = Gallery.delay + $.on delayInput, 'change', Gallery.cb.setDelay + $.on delayInput, 'change', $.cb.value + subEntries.push el: delayLabel + + subEntries \ No newline at end of file diff --git a/src/Images/ImageCommon.coffee b/src/Images/ImageCommon.coffee new file mode 100644 index 000000000..2c55fbbbf --- /dev/null +++ b/src/Images/ImageCommon.coffee @@ -0,0 +1,81 @@ +ImageCommon = + rewind: (el) -> + if el.nodeName is 'VIDEO' + el.currentTime = 0 if el.readyState >= el.HAVE_METADATA + else if /\.gif$/.test el.src + $.queueTask -> el.src = el.src + + pushCache: (el) -> + ImageCommon.cache = el + $.on el, 'error', ImageCommon.cacheError + + popCache: -> + el = ImageCommon.cache + $.off el, 'error', ImageCommon.cacheError + delete ImageCommon.cache + el + + cacheError: -> + delete ImageCommon.cache if ImageCommon.cache is @ + + decodeError: (file, post) -> + return false unless file.error?.code is MediaError.MEDIA_ERR_DECODE + unless message = $ '.warning', post.file.thumb.parentNode + message = $.el 'div', className: 'warning' + $.after post.file.thumb, message + message.textContent = 'Error: Corrupt or unplayable video' + return true + + error: (file, post, delay, cb) -> + src = post.file.URL.split '/' + URL = Redirect.to 'file', + boardID: post.board.ID + filename: src[src.length - 1] + unless Conf['404 Redirect'] and URL and Redirect.securityCheck URL + URL = null + + return cb URL if (post.isDead or post.file.isDead) and file.src.split('/')[2] is 'i.4cdn.org' + + timeoutID = setTimeout (-> cb URL), delay if delay? + return if post.isDead or post.file.isDead + redirect = -> + if file.src.split('/')[2] is 'i.4cdn.org' + clearTimeout timeoutID if delay? + cb URL + + <% if (type === 'crx') { %> + $.ajax post.file.URL, + onloadend: -> + if @status is 200 + URL = post.file.URL + else + post.kill true if @status is 404 + redirect() + , + type: 'head' + <% } else { %> + # XXX CORS for i.4cdn.org WHEN? + $.ajax "//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: -> + post.kill() if @status is 404 + return redirect() if @status isnt 200 + for postObj in @response.posts + break if postObj.no is post.ID + if postObj.no isnt post.ID + post.kill() + redirect() + else if postObj.filedeleted + post.kill true + redirect() + else + URL = post.file.URL + <% } %> + + # Add controls, but not until the mouse is moved over the video. + addControls: (video) -> + handler = -> + $.off video, 'mouseover', handler + # Hacky workaround for Firefox forever-loading bug for very short videos + t = new Date().getTime() + $.asap (-> chrome? or (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or new Date().getTime() >= t + 1000), -> + video.controls = true + $.on video, 'mouseover', handler