diff --git a/assets/css/misuzu/embed.css b/assets/css/misuzu/embed.css
new file mode 100644
index 00000000..70e4e630
--- /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 1489c477..a07d1669 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 00000000..27a817a5
--- /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 eabe26fc..c4735a8a 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 c7850e6f..1bc2d277 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
);