diff --git a/assets/css/misuzu/embed.css b/assets/css/misuzu/embed.css new file mode 100644 index 0000000..70e4e63 --- /dev/null +++ b/assets/css/misuzu/embed.css @@ -0,0 +1,167 @@ +.embed { + display: inline-block; + overflow: hidden; +} + +.embed iframe { + width: 100%; + height: 100%; + display: block; +} + +.embedph { + display: inline-block; + overflow: hidden; + cursor: pointer; + color: var(--text-colour); + text-decoration: none; +} +.embedph:hover .embedph-bg img, +.embedph:active .embedph-bg img, +.embedph:focus .embedph-bg img, +.embedph:focus-within .embedph-bg img { + transform: scale(1.1); + filter: blur(10px) brightness(80%); +} +.embedph:hover .embedph-info, +.embedph:active .embedph-info, +.embedph:focus .embedph-info, +.embedph:focus-within .embedph-info { + opacity: 0; +} +.embedph:hover .embedph-play, +.embedph:active .embedph-play, +.embedph:focus .embedph-play, +.embedph:focus-within .embedph-play { + opacity: 1; +} +.embedph-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; +} +.embedph-bg img { + object-fit: cover; + width: 100%; + height: 100%; + display: inline-block; + will-change: transform, filter; + transition: transform .2s, filter .2s; +} +.embedph-fg { + width: 100%; + height: 100%; +} + +.embedph-info { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: flex-end; + will-change: opacity; + transition: opacity .2s; +} +.embedph-info-wrap { + margin: 5px; + background-color: var(--background-colour-translucent-8); + border-radius: 5px; + display: flex; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} +.embedph-info-bar { + width: 5px; + margin: 5px; + border-radius: 5px; + flex: 0 0 auto; + background-color: var(--embedph-colour, var(--accent-colour)); +} +.embedph-info-body { + margin: 10px; + margin-left: 5px; +} +.embedph-info-title { + font-size: 2em; + font-weight: 400; + line-height: 1.1em; + margin-bottom: 5px; +} +.embedph-info-desc { + line-height: 1.4em; + margin: .5em 0; +} +.embedph-info-site { + font-size: .9em; + line-height: 1.2em; +} + +@media (max-width: 640px) { + .embedph-info-title { + font-size: 1.5em; + line-height: 1.2em; + } + .embedph-info-desc { + display: none; + } +} + +.embedph-play { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + opacity: 0; + will-change: opacity; + transition: opacity .2s; +} +.embedph-play-internal { + margin-top: 40px; + margin-bottom: 20px; +} +.embedph-play-external { + padding: 5px 10px; + font-size: 1.2em; + line-height: 1.5em; + text-decoration: none; + color: var(--text-colour); + background-color: var(--background-colour-translucent-6); + border-radius: 5px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + will-change: background-color; + transition: background-color .2s, transform .2s; +} +.embedph-play-external:hover, +.embedph-play-external:focus { + background-color: var(--background-colour-translucent-8); + transform: scale(1.2); +} + +.embed-youtube, +.embedph-youtube { + aspect-ratio: 16 / 9; + width: 100%; + height: 100%; + max-width: 560px; + max-height: 315px; +} + +.embed-nicovideo, +.embedph-nicovideo { + aspect-ratio: 16 / 9; + width: 100%; + height: 100%; + max-width: 640px; + max-height: 360px; +} diff --git a/assets/js/misuzu/_main.js b/assets/js/misuzu/_main.js index 1489c47..a07d166 100644 --- a/assets/js/misuzu/_main.js +++ b/assets/js/misuzu/_main.js @@ -1,4 +1,6 @@ var Misuzu = function() { + const UIHARU_API = location.protocol + '//uiharu.' + location.host; + timeago.render($qa('time')); hljs.initHighlighting(); @@ -6,6 +8,268 @@ var Misuzu = function() { Misuzu.Forum.Editor.init(); Misuzu.Events.dispatch(); Misuzu.initLoginPage(); + + const embeds = Array.from($qa('.js-msz-embed-media')); + if(embeds.length > 0) { + $as(embeds); + + const uiharu = new Uiharu(UIHARU_API) + elems = new Map(embeds.map(function(elem) { return [elem.dataset.mszEmbedUrl, elem]; })); + + uiharu.lookupMany(Array.from(elems.keys()), function(resp) { + if(resp.results === undefined) + return; // rip + + for(const result of resp.results) { + if(result.error) { + console.error(result.error); + continue; + } + + if(result.info.title === undefined) { + console.warn('Media is no longer available.'); + continue; + } + + let elem = elems.get(result.url); + + (function(elem, info) { + const replaceElement = function(body) { + $ib(elem, body); + $r(elem); + elem = body; + }; + + const createEmbedPH = function(type, info, onclick, width, height) { + let infoChildren = []; + + infoChildren.push({ + tag: 'h1', + attrs: { + className: 'embedph-info-title', + }, + child: info.title, + }); + + if(info.description) { + let firstLine = info.description.split("\n")[0].trim(); + if(firstLine.length > 300) + firstLine = firstLine.substring(0, 300).trim() + '...'; + + infoChildren.push({ + tag: 'div', + attrs: { + className: 'embedph-info-desc', + }, + child: firstLine, + }); + } + + infoChildren.push({ + tag: 'div', + attrs: { + className: 'embedph-info-site', + }, + child: info.site_name, + }); + + let style = info.color === undefined ? '' : ('--embedph-colour: ' + info.color); + + if(width !== undefined) + style += 'width: ' + width.toString() + ';'; + if(height !== undefined) + style += 'height: ' + height.toString() + ';'; + + let bgElem; + if(info.image !== undefined) { + bgElem = { + tag: 'img', + attrs: { + src: info.image, + }, + }; + } else { + bgElem = {}; + } + + return $e({ + attrs: { + href: 'javascript:void(0);', + className: ('embedph embedph-' + type), + style: style, + }, + child: [ + { + attrs: { + className: 'embedph-bg', + }, + child: bgElem, + }, + { + attrs: { + className: 'embedph-fg', + }, + child: [ + { + attrs: { + className: 'embedph-info', + }, + child: { + attrs: { + className: 'embedph-info-wrap', + }, + child: [ + { + attrs: { + className: 'embedph-info-bar', + }, + }, + { + attrs: { + className: 'embedph-info-body', + }, + child: infoChildren, + } + ], + }, + }, + { + attrs: { + className: 'embedph-play', + onclick: function(ev) { + if(ev.target.tagName.toLowerCase() !== 'a') + onclick(ev); + }, + }, + child: [ + { + attrs: { + className: 'embedph-play-internal', + }, + child: { + tag: 'i', + attrs: { + className: 'fas fa-play fa-4x fa-fw', + }, + }, + }, + { + tag: 'a', + attrs: { + className: 'embedph-play-external', + href: info.url, + target: '_blank', + rel: 'noopener', + }, + child: ('or watch on ' + info.site_name + '?'), + } + ], + } + ], + }, + ], + }); + }; + + if(info.type === 'youtube:video') { + let embedUrl = 'https://www.youtube.com/embed/' + info.youtube_video_id + '?rel=0&autoplay=1'; + + if(info.youtube_start_time) + embedUrl += '&t=' + encodeURIComponent(info.youtube_start_time); + + if(info.youtube_playlist) { + embedUrl += '&list=' + encodeURIComponent(info.youtube_playlist); + + if(info.youtube_playlist_index) + embedUrl += '&index=' + encodeURIComponent(info.youtube_playlist_index); + } + + replaceElement(createEmbedPH('youtube', info, function() { + replaceElement($e({ + attrs: { + className: 'embed embed-youtube', + }, + child: { + tag: 'iframe', + attrs: { + frameborder: 0, + allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', + allowfullscreen: 'allowfullscreen', + src: embedUrl, + }, + }, + })); + })); + } else if(info.type === 'niconico:video') { + let embedUrl = 'https://embed.nicovideo.jp/watch/' + info.nicovideo_video_id + '/script?w=100%25&h=100%25&autoplay=1'; + + if(info.nicovideo_start_time) + embedUrl += '&from=' + encodeURIComponent(info.nicovideo_start_time); + + replaceElement(createEmbedPH('nicovideo', info, function() { + replaceElement($e({ + attrs: { + className: 'embed embed-nicovideo', + }, + child: { + tag: 'script', + attrs: { + async: 'async', + src: embedUrl, + }, + }, + })); + })); + } else if(info.type === 'media') { + if(info.is_video) { + let width = info.width, + height = info.height; + + const gcd = function(a, b) { + return (b == 0) ? a : gcd(b, a % b); + }; + + let ratio = gcd(width, height), + widthRatio = width / ratio, + heightRatio = height / ratio; + + if(width > height) { + width = Math.min(640, width); + height = Math.ceil((width / widthRatio) * heightRatio).toString() + 'px'; + width = width.toString() + 'px'; + } else { + height = Math.min(360, height); + width = Math.ceil((height / heightRatio) * widthRatio).toString() + 'px'; + height = height.toString() + 'px'; + } + + replaceElement(createEmbedPH('external', info, function() { + replaceElement($e({ + attrs: { + className: 'embed embed-external', + }, + child: { + tag: 'video', + attrs: { + autoplay: 'autoplay', + controls: 'controls', + src: info.url, + style: { + width: width, + height: height, + }, + }, + }, + })); + }, width, height)); + } else if(info.is_audio) { + // coming someday + } + } + })(elem, result.info); + } + }); + } }; Misuzu.showMessageBox = function(text, title, buttons) { if($q('.messagebox')) diff --git a/assets/js/misuzu/uiharu.js b/assets/js/misuzu/uiharu.js new file mode 100644 index 0000000..27a817a --- /dev/null +++ b/assets/js/misuzu/uiharu.js @@ -0,0 +1,58 @@ +const Uiharu = function(apiUrl) { + const maxBatchSize = 5; + const lookupOneUrl = apiUrl + '/metadata', + lookupManyUrl = apiUrl + '/metadata/batch'; + + const lookupManyInternal = function(targetUrls, callback) { + const formData = new FormData; + + for(const url of targetUrls) + formData.append('url[]', url); + + const xhr = new XMLHttpRequest; + xhr.addEventListener('load', function() { + callback(JSON.parse(xhr.responseText)); + }); + xhr.addEventListener('error', function(ev) { + callback({ status: xhr.status, error: 'xhr', details: ev }); + }); + xhr.open('POST', lookupManyUrl); + xhr.send(formData); + }; + + return { + lookupOne: function(targetUrl, callback) { + if(typeof callback !== 'function') + throw 'callback is missing'; + targetUrl = (targetUrl || '').toString(); + if(targetUrl.length < 1) + return; + + const xhr = new XMLHttpRequest; + xhr.addEventListener('load', function() { + callback(JSON.parse(xhr.responseText)); + }); + xhr.addEventListener('error', function() { + callback({ status: xhr.status, error: 'xhr', details: ex }); + }); + xhr.open('POST', lookupOneUrl); + xhr.send(targetUrl); + }, + lookupMany: function(targetUrls, callback) { + if(!Array.isArray(targetUrls)) + throw 'targetUrls must be an array of urls'; + if(typeof callback !== 'function') + throw 'callback is missing'; + if(targetUrls < 1) + return; + + if(targetUrls.length <= maxBatchSize) { + lookupManyInternal(targetUrls, callback); + return; + } + + for(let i = 0; i < targetUrls.length; i += maxBatchSize) + lookupManyInternal(targetUrls.slice(i, i + maxBatchSize), callback); + }, + }; +}; diff --git a/src/Parsers/BBCode/Tags/AudioTag.php b/src/Parsers/BBCode/Tags/AudioTag.php index eabe26f..c4735a8 100644 --- a/src/Parsers/BBCode/Tags/AudioTag.php +++ b/src/Parsers/BBCode/Tags/AudioTag.php @@ -10,13 +10,11 @@ final class AudioTag extends BBCodeTag { function ($matches) { $url = parse_url($matches[1]); - if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true)) { + if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true)) return $matches[0]; - } - //$url['host'] = mb_strtolower($url['host']); + //return sprintf('%1$s', $matches[1]); - //$mediaUrl = url_proxy_media($matches[1]); $mediaUrl = $matches[1]; return ""; }, diff --git a/src/Parsers/BBCode/Tags/VideoTag.php b/src/Parsers/BBCode/Tags/VideoTag.php index c7850e6..1bc2d27 100644 --- a/src/Parsers/BBCode/Tags/VideoTag.php +++ b/src/Parsers/BBCode/Tags/VideoTag.php @@ -4,48 +4,16 @@ namespace Misuzu\Parsers\BBCode\Tags; use Misuzu\Parsers\BBCode\BBCodeTag; final class VideoTag extends BBCodeTag { - private const YOUTUBE_REGEX = '#^(?:www\.)?youtube(?:-nocookie)?\.(?:[a-z]{2,63})$#u'; - private const YOUTUBE_EMBED = ''; - - private const NICODOUGA_EMBED = ''; - public function parseText(string $text): string { return preg_replace_callback( '#\[video\]((?:https?:\/\/).+?)\[/video\]#', function ($matches) { $url = parse_url($matches[1]); - if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true)) { + if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true)) return $matches[0]; - } - $url['host'] = mb_strtolower($url['host']); - - // support youtube playlists? - - if($url['host'] === 'youtu.be' || $url['host'] === 'www.youtu.be') { - return sprintf(self::YOUTUBE_EMBED, $url['path']); - } - - if(!empty($url['query']) && ($url['path'] ?? '') === '/watch' && preg_match(self::YOUTUBE_REGEX, $url['host'])) { - parse_str(html_entity_decode($url['query']), $ytQuery); - - if(!empty($ytQuery['v']) && preg_match('#^([a-zA-Z0-9_-]+)$#u', $ytQuery['v'])) { - return sprintf(self::YOUTUBE_EMBED, $ytQuery['v']); - } - } - - if($url['host'] === 'nicovideo.jp' || $url['host'] === 'www.nicovideo.jp') { - $splitPath = explode('/', trim($url['path'], '/')); - - if(count($splitPath) > 1 && $splitPath[0] === 'watch') { - return sprintf(self::NICODOUGA_EMBED, $splitPath[1]); - } - } - - //$mediaUrl = url_proxy_media($matches[1]); - $mediaUrl = $matches[1]; - return sprintf('', $mediaUrl); + return sprintf('%1$s', $matches[1]); }, $text );