From e6e5c873f6a034884a65a77b90d0181c9dc7f8ca Mon Sep 17 00:00:00 2001 From: flashwave Date: Sun, 29 Jan 2023 20:29:20 +0000 Subject: [PATCH] Audio embed previews (no player yet). --- assets/css/misuzu/embed.css | 159 ++++++++++++ assets/js/misuzu/aembed.js | 365 +++++++++++++++++++++++++++ assets/js/misuzu/embed.js | 3 + assets/js/misuzu/vembed.js | 64 ++++- src/Parsers/BBCode/Tags/AudioTag.php | 12 +- 5 files changed, 593 insertions(+), 10 deletions(-) create mode 100644 assets/js/misuzu/aembed.js diff --git a/assets/css/misuzu/embed.css b/assets/css/misuzu/embed.css index 01f8351..0a67b2c 100644 --- a/assets/css/misuzu/embed.css +++ b/assets/css/misuzu/embed.css @@ -204,3 +204,162 @@ -webkit-backdrop-filter: blur(10px); transition: opacity .2s, transform .2s; } + +.aembed { + display: inline-block; + overflow: hidden; + text-shadow: initial; +} + +.aembedph { + display: inline-block; + overflow: hidden; + cursor: pointer; + color: var(--text-colour); + text-decoration: none; + text-shadow: initial; + max-width: 500px; + min-width: 300px; + height: 70px; + border-radius: 5px; + margin: 5px; +} +.aembedph:hover .aembedph-play, +.aembedph:active .aembedph-play, +.aembedph:focus .aembedph-play, +.aembedph:focus-within .aembedph-play { + opacity: 1; +} +.aembedph-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; +} +.aembedph-bg-none { + background: var(--background-pattern); + background-color: var(--aembedph-colour, var(--accent-colour)); + background-blend-mode: multiply; +} +.aembedph-bg img { + object-fit: cover; + width: 100%; + height: 100%; + display: inline-block; + transform: scale(1.1); + filter: blur(10px) brightness(80%); +} +.aembedph-fg { + width: 100%; + height: 100%; +} + +.aembedph-info { + display: flex; + background-color: var(--background-colour-translucent-5); + align-items: center; + width: 100%; + height: 100%; + padding: 5px; +} +.aembedph-info-cover { + flex: 0 0 auto; + overflow: hidden; + border-radius: 5px; +} +.aembedph-info-cover-none { + background-color: var(--aembedph-colour, var(--accent-colour)); + width: 5px; + height: 100%; +} +.aembedph-info-cover img { + max-width: 60px; + max-height: 60px; + display: inline-block; + vertical-align: middle; +} +.aembedph-info-cover-none img { + display: none; +} +.aembedph-info-body { + padding: 0 5px; +} +.aembedph-info-title { + font-size: 1.4em; + font-weight: 400; + line-height: 1.2em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 430px; +} +.aembedph-info-title-artist { + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 700; +} +.aembedph-info-album { + line-height: 1.4em; + word-break: break-word; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 430px; +} +.aembedph-info-site { + font-size: .9em; + line-height: 1.2em; +} + +.aembedph-play { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity .2s; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); +} +.aembedph-play-internal { + width: 70px; + height: 70px; + text-align: center; + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; +} +.aembedph-play-external { + flex: 1 1 auto; + display: flex; + justify-content: flex-end; + align-items: center; + padding: 5px; +} +.aembedph-play-external-link { + padding: 2px 5px; + 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); + transition: background-color .2s; +} +.aembedph-play-external-link:hover, +.aembedph-play-external-link:focus { + background-color: var(--background-colour-translucent-8); +} +.aembedph-play-external-link:active { + background-color: var(--background-colour-translucent-5); +} diff --git a/assets/js/misuzu/aembed.js b/assets/js/misuzu/aembed.js new file mode 100644 index 0000000..ee0fbd9 --- /dev/null +++ b/assets/js/misuzu/aembed.js @@ -0,0 +1,365 @@ +const MszAudioEmbedPlayerEvents = function() { + return [ + 'play', 'pause', 'stop', + 'mute', 'volume', + 'rate', 'duration', 'time', + ]; +}; + +const MszAudioEmbed = function(player) { + const elem = $e({ + attrs: { + classList: ['aembed', 'aembed-' + player.getType()], + }, + child: player, + }); + + return { + getElement: function() { + return elem; + }, + appendTo: function(target) { + target.appendChild(elem); + }, + insertBefore: function(ref) { + $ib(ref, elem); + }, + nuke: function() { + $r(elem); + }, + replaceElement(target) { + $ib(target, elem); + $r(target); + }, + getPlayer: function() { + return player; + }, + }; +}; + +const MszAudioEmbedPlayer = function(metadata, options) { + options = options || {}; + + const shouldAutoplay = options.autoplay === undefined || options.autoplay, + haveNativeControls = options.nativeControls !== undefined && options.nativeControls; + + const playerAttrs = { + src: metadata.url, + style: {}, + }; + + if(shouldAutoplay) + playerAttrs.autoplay = 'autoplay'; + if(haveNativeControls) + playerAttrs.controls = 'controls'; + + const watchers = new MszWatcherCollection; + watchers.define(MszAudioEmbedPlayerEvents()); + + const player = $e({ + tag: 'audio', + attrs: playerAttrs, + }); + + const pub = { + getElement: function() { + return player; + }, + appendTo: function(target) { + target.appendChild(player); + }, + insertBefore: function(ref) { + $ib(ref, player); + }, + nuke: function() { + $r(player); + }, + replaceElement(target) { + $ib(target, player); + $r(target); + }, + getType: function() { return 'external'; }, + }; + + watchers.proxy(pub); + + player.addEventListener('play', function() { watchers.call('play', pub); }); + + const pPlay = function() { player.play(); }; + pub.play = pPlay; + + const pPause = function() { player.pause(); }; + pub.pause = pPause; + + let stopCalled = false; + player.addEventListener('pause', function() { + watchers.call(stopCalled ? 'stop' : 'pause', pub); + stopCalled = false; + }); + + const pStop = function() { + stopCalled = true; + player.pause(); + player.currentTime = 0; + }; + pub.stop = pStop; + + const pIsPlaying = function() { return !player.paused; }; + pub.isPlaying = pIsPlaying; + + const pIsMuted = function() { return player.muted; }; + pub.isMuted = pIsMuted; + + let lastMuteState = player.muted; + player.addEventListener('volumechange', function() { + if(lastMuteState !== player.muted) { + lastMuteState = player.muted; + watchers.call('mute', pub, [lastMuteState]); + } else + watchers.call('volume', pub, [player.volume]); + }); + + const pSetMuted = function(state) { player.muted = state; }; + pub.setMuted = pSetMuted; + + const pGetVolume = function() { return player.volume; }; + pub.getVolume = pGetVolume; + + const pSetVolume = function(volume) { player.volume = volume; }; + pub.setVolume = pSetVolume; + + const pGetPlaybackRate = function() { return player.playbackRate; }; + pub.getPlaybackRate = pGetPlaybackRate; + + player.addEventListener('ratechange', function() { + watchers.call('rate', pub, [player.playbackRate]); + }); + + const pSetPlaybackRate = function(rate) { player.playbackRate = rate; }; + pub.setPlaybackRate = pSetPlaybackRate; + + window.addEventListener('durationchange', function() { + watchers.call('duration', pub, [player.duration]); + }); + + const pGetDuration = function() { return player.duration; }; + pub.getDuration = pGetDuration; + + window.addEventListener('timeupdate', function() { + watchers.call('time', pub, [player.currentTime]); + }); + + const pGetTime = function() { return player.currentTime; }; + pub.getTime = pGetTime; + + const pSeek = function(time) { player.currentTime = time; }; + pub.seek = pSeek; + + return pub; +}; + +const MszAudioEmbedPlaceholder = function(metadata, options) { + options = options || {}; + + if(typeof options.player !== 'function' && typeof options.onclick !== 'function') + throw 'Neither a player nor an onclick handler were provided.'; + + let title = [], + album = undefined; + if(metadata.media !== undefined && metadata.media.tags !== undefined) { + const tags = metadata.media.tags; + + if(tags.title !== undefined) { + if(tags.artist !== undefined) { + title.push({ + tag: 'span', + attrs: { + className: 'aembedph-info-title-artist', + }, + child: tags.artist, + }); + title.push(' - '); + } + + title.push({ + tag: 'span', + attrs: { + className: 'aembedph-info-title-title', + }, + child: tags.title, + }); + } else { + title.push({ + tag: 'span', + attrs: { + className: 'aembedph-info-title-title', + }, + child: metadata.title, + }); + } + + if(tags.album !== undefined && tags.album !== tags.title) + album = tags.album; + } + + const infoChildren = []; + + infoChildren.push({ + tag: 'h1', + attrs: { + className: 'aembedph-info-title', + }, + child: title, + }); + + infoChildren.push({ + tags: 'p', + attrs: { + className: 'aembedph-info-album', + }, + child: album, + }); + + infoChildren.push({ + tag: 'div', + attrs: { + className: 'aembedph-info-site', + }, + child: metadata.site_name, + }); + + const style = []; + if(typeof metadata.color !== 'undefined') + style.push('--aembedph-colour: ' + metadata.color); + + const coverBackground = $e({ + attrs: { + className: 'aembedph-bg', + }, + child: { + tag: 'img', + attrs: { + alt: '', + src: metadata.image, + onerror: function(ev) { + coverBackground.classList.add('aembedph-bg-none'); + }, + }, + }, + }); + + const coverPreview = $e({ + attrs: { + className: 'aembedph-info-cover', + }, + child: { + tag: 'img', + attrs: { + alt: '', + src: metadata.image, + onerror: function(ev) { + coverPreview.classList.add('aembedph-info-cover-none'); + }, + }, + }, + }); + + const pub = {}; + + const elem = $e({ + attrs: { + className: ('aembedph aembedph-' + (options.type || 'external')), + style: style.join(';'), + title: metadata.title, + }, + child: [ + coverBackground, + { + attrs: { + className: 'aembedph-fg', + }, + child: [ + { + attrs: { + className: 'aembedph-info', + }, + child: [ + coverPreview, + { + attrs: { + className: 'aembedph-info-body', + }, + child: infoChildren, + } + ], + }, + { + attrs: { + className: 'aembedph-play', + onclick: function(ev) { + if(ev.target.tagName.toLowerCase() === 'a') + return; + + if(typeof options.onclick === 'function') { + options.onclick(ev); + return; + } + + const player = new options.player(metadata, options); + + const embed = new MszAudioEmbed(player); + if(options.autoembed === undefined || options.autoembed) + embed.replaceElement(elem); + + if(typeof options.onembed === 'function') + options.onembed(embed); + }, + }, + child: [ + { + attrs: { + className: 'aembedph-play-internal', + }, + child: { + tag: 'i', + attrs: { + className: 'fas fa-play fa-3x fa-fw', + }, + }, + }, + { + attrs: { + className: 'aembedph-play-external', + }, + child: { + tag: 'a', + attrs: { + className: 'aembedph-play-external-link', + href: metadata.url, + target: '_blank', + rel: 'noopener', + }, + child: ('or listen on ' + metadata.site_name + '?') + }, + } + ], + } + ], + }, + ], + }); + + pub.getElement = function() { return elem; }; + pub.appendTo = function(target) { target.appendChild(elem); }; + pub.insertBefore = function(ref) { $ib(ref, elem); }; + pub.nuke = function() { + $r(elem); + elem = null; + }; + pub.replaceElement = function(target) { + $ib(target, elem); + $r(target); + }; + + return pub; +} diff --git a/assets/js/misuzu/embed.js b/assets/js/misuzu/embed.js index 88fe365..9af9d83 100644 --- a/assets/js/misuzu/embed.js +++ b/assets/js/misuzu/embed.js @@ -97,7 +97,10 @@ var MszEmbed = (function() { options.maxWidth = 640; options.maxHeight = 360; } else if(metadata.is_audio) { + phc = MszAudioEmbedPlaceholder; options.type = 'external'; + options.player = MszAudioEmbedPlayer; + options.nativeControls = true; } else if(metadata.is_image) { options.type = 'external'; } diff --git a/assets/js/misuzu/vembed.js b/assets/js/misuzu/vembed.js index 665f58c..59960e4 100644 --- a/assets/js/misuzu/vembed.js +++ b/assets/js/misuzu/vembed.js @@ -81,6 +81,61 @@ const MszVideoEmbed = function(playerOrFrame) { const MszVideoEmbedFrame = function(player, options) { options = options || {}; + const icoStatePlay = 'fa-play', + icoStatePause = 'fa-pause', + icoStateStop = 'fa-stop'; + const icoVolMute = 'fa-volume-mute', + icoVolOff = 'fa-volume-off', + icoVolQuiet = 'fa-volume-down', + icoVolLoud = 'fa-volume-up'; + + const btnPlayPause = $e({ + attrs: {}, + child: { + tag: 'i', + attrs: { + classList: ['fas', 'fa-fw', icoStatePlay], + }, + } + }); + + const btnStop = $e({ + attrs: {}, + child: { + tag: 'i', + attrs: { + classList: ['fas', 'fa-fw', icoStateStop], + }, + }, + }); + + const numCurrentTime = $e({ + attrs: {}, + }); + + const sldProgress = $e({ + attrs: {}, + child: [], + }); + + const numDurationRemaining = $e({ + attrs: {}, + }); + + const btnVolMute = $e({ + attrs: {}, + child: { + tag: 'i', + attrs: { + // isMuted === icoVolMute + // vol < 0.01 === icoVolOff + // vol < 0.5 === icoVolQuiet + // vol < 1.0 === icoVolLoud + classList: ['fas', 'fa-fw', icoVolLoud], + }, + }, + }); + const elem = $e({ attrs: { className: 'embedvf', @@ -106,7 +161,11 @@ const MszVideoEmbedFrame = function(player, options) { className: 'embedvf-controls', }, child: [ - 'Play/Pause Stop [|---------] 1.00x 100%', + btnPlayPause, + btnStop, + numCurrentTime, + sldProgress, + numDurationRemaining, ], }, ], @@ -728,7 +787,6 @@ const MszVideoEmbedPlaceholder = function(metadata, options) { const elem = $e({ attrs: { - href: 'javascript:void(0);', className: ('embedph embedph-' + (options.type || 'external')), style: style.join(';'), }, @@ -821,7 +879,7 @@ const MszVideoEmbedPlaceholder = function(metadata, options) { child: ('or watch on ' + metadata.site_name + '?'), } ], - } + }, ], }, ], diff --git a/src/Parsers/BBCode/Tags/AudioTag.php b/src/Parsers/BBCode/Tags/AudioTag.php index e96f598..c78a06c 100644 --- a/src/Parsers/BBCode/Tags/AudioTag.php +++ b/src/Parsers/BBCode/Tags/AudioTag.php @@ -13,13 +13,11 @@ final class AudioTag extends BBCodeTag { if(empty($url['scheme']) || !in_array(mb_strtolower($url['scheme']), ['http', 'https'], true)) return $matches[0]; - // return sprintf( - // '' - // . '', - // $matches[1] - // ); - - return ""; + return sprintf( + '' + . '', + $matches[1] + ); }, $text );