Merge remote-tracking branch 'upstream/master' into save-volume-settings

This commit is contained in:
Edgardo Ramírez
2020-10-06 14:55:56 -05:00
92 changed files with 68526 additions and 258 deletions

View File

@@ -3,20 +3,19 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<link href="./styles/chat.css" rel="stylesheet" />
<link href="./styles/standalone-chat.css" rel="stylesheet" />
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js"></script>
</head>
<body>
<div id="messages-only"></div>
<script type="module">
import { render, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import StandaloneChat from './js/app-standalone-chat.js';
render(
html`<${StandaloneChat} messagesOnly />`, document.getElementById("messages-only")

View File

@@ -3,17 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<link href="//unpkg.com/video.js@7.9.3/dist/video-js.css" rel="stylesheet"/>
<link href="https://unpkg.com/@videojs/themes@1/dist/fantasy/index.css" rel="stylesheet" />
<script src="//unpkg.com/video.js@7.9.3/dist/alt/video.core.min.js"></script>
<script src="//unpkg.com/@videojs/http-streaming@2.1.0/dist/videojs-http-streaming.min.js"></script>
<link href="/js/web_modules/videojs/dist/video-js.min.css" rel="stylesheet"/>
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
<link href="./styles/video.css" rel="stylesheet" />
<link href="./styles/video-only.css" rel="stylesheet" />
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
</head>
<body>
@@ -21,7 +17,10 @@
<div id="video-only"></div>
<script type="module">
import { render, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import VideoOnly from './js/app-video-only.js';
render(html`<${VideoOnly} />`, document.getElementById("video-only"));
</script>

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Owncast</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
@@ -23,35 +24,25 @@
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"/>
<link href="//unpkg.com/video.js@7.9.3/dist/video-js.css" rel="stylesheet" />
<link href="https://unpkg.com/@videojs/themes@1/dist/fantasy/index.css" rel="stylesheet" />
<script src="//unpkg.com/video.js@7.9.3/dist/alt/video.core.min.js" defer></script>
<script src="//unpkg.com/@videojs/http-streaming@2.1.0/dist/videojs-http-streaming.min.js" defer></script>
<!-- markdown renderer -->
<script src="//unpkg.com/showdown/dist/showdown.min.js" defer></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js" defer></script>
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<link href="/js/web_modules/videojs/dist/video-js.min.css" rel="stylesheet"/>
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
<link href="./styles/video.css" rel="stylesheet" />
<link href="./styles/chat.css" rel="stylesheet" />
<link href="./styles/user-content.css" rel="stylesheet" />
<link href="./styles/app.css" rel="stylesheet" />
<!-- Preloads -->
<link rel="preconnect" href="https://unpkg.com/@joeattardi/emoji-button@4.2.0/dist/index.js" />
<link rel="preconnect" href="https://unpkg.com/preact?module" />
<link rel="preconnect" href="https://unpkg.com/htm?module" />
</head>
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app"></div>
<script type="module">
import { render, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
import { h, render } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import App from './js/app.js';
render(html`<${App} />`, document.getElementById("app"));
</script>
@@ -77,7 +68,7 @@
<img src="https://owncast.online/images/logo.png" />
<br/>
<p>
This <a href="https://owncast.online" target="_blank">Owncast</a> stream requires Javascript to play.
This <a href="https://owncast.online" rel="noopener noreferrer" target="_blank">Owncast</a> stream requires Javascript to play.
</p>
</div>
</noscript>

View File

@@ -1,5 +1,5 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import Chat from './components/chat/chat.js';

View File

@@ -1,5 +1,5 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { OwncastPlayer } from './components/player.js';

View File

@@ -1,6 +1,7 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import showdown from '/js/web_modules/showdown.js';
import { OwncastPlayer } from './components/player.js';
import SocialIcon from './components/social.js';

View File

@@ -1,8 +1,9 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { EmojiButton } from 'https://unpkg.com/@joeattardi/emoji-button@4.2.0/dist/index.js';
import { EmojiButton } from '/js/web_modules/@joeattardi/emoji-button.js';
import ContentEditable, { replaceCaret } from './content-editable.js';
import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js';
import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js';

View File

@@ -1,5 +1,5 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import Message from './message.js';

View File

@@ -6,7 +6,7 @@ and here:
https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103
*/
import { Component, createRef, h } from 'https://unpkg.com/preact?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
export function replaceCaret(el) {
// Place the caret at the end of the element

View File

@@ -1,9 +1,9 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { messageBubbleColorForString } from '../../utils/user-colors.js';
import { formatMessageText } from '../../utils/chat.js';
import { formatMessageText, formatTimestamp } from '../../utils/chat.js';
import { generateAvatar } from '../../utils/helpers.js';
import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
@@ -13,9 +13,10 @@ export default class Message extends Component {
const { type } = message;
if (type === SOCKET_MESSAGE_TYPES.CHAT) {
const { image, author, body } = message;
const { image, author, body, timestamp } = message;
const formattedMessage = formatMessageText(body, username);
const avatar = image || generateAvatar(author);
const formattedTimestamp = formatTimestamp(timestamp);
const authorColor = messageBubbleColorForString(author);
const avatarBgColor = { backgroundColor: authorColor };
@@ -35,6 +36,7 @@ export default class Message extends Component {
</div>
<div
class="message-text text-gray-300 font-normal overflow-y-hidden"
title=${formattedTimestamp}
dangerouslySetInnerHTML=${
{ __html: formattedMessage }
}

View File

@@ -1,5 +1,5 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { generateAvatar, setLocalStorage } from '../../utils/helpers.js';

View File

@@ -1,5 +1,7 @@
// https://docs.videojs.com/player
import videojs from '/js/web_modules/videojs/dist/video.min.js';
const VIDEO_ID = 'video';
// TODO: This directory is customizable in the config. So we should expose this via the config API.
const URL_STREAM = `/hls/stream.m3u8`;
@@ -53,13 +55,14 @@ class OwncastPlayer {
}
init() {
videojs.Vhs.xhr.beforeRequest = function (options) {
this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS);
this.vjsPlayer.beforeRequest = function (options) {
const cachebuster = Math.round(new Date().getTime() / 1000);
options.uri = `${options.uri}?cachebust=${cachebuster}`;
return options;
};
this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS);
this.addAirplay();
this.vjsPlayer.ready(this.handleReady);
}
@@ -76,11 +79,11 @@ class OwncastPlayer {
// play
startPlayer() {
this.log('Start playing');
const source = { ...VIDEO_SRC }
const source = { ...VIDEO_SRC };
this.vjsPlayer.volume(localStorage.getItem('owncastVolume'));
this.vjsPlayer.src(source);
// this.vjsPlayer.play();
};
}
handleReady() {
this.log('on Ready');
@@ -124,7 +127,7 @@ class OwncastPlayer {
setPoster() {
const cachebuster = Math.round(new Date().getTime() / 1000);
const poster = POSTER_THUMB + "?okhi=" + cachebuster;
const poster = POSTER_THUMB + '?okhi=' + cachebuster;
this.vjsPlayer.poster(poster);
}
@@ -138,7 +141,6 @@ class OwncastPlayer {
if (window.WebKitPlaybackTargetAvailabilityEvent) {
var videoJsButtonClass = videojs.getComponent('Button');
var concreteButtonClass = videojs.extend(videoJsButtonClass, {
// The `init()` method will also work for constructor logic here, but it is
// deprecated. If you provide an `init()` method, it will override the
// `constructor()` method!
@@ -152,8 +154,10 @@ class OwncastPlayer {
},
});
var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(new concreteButtonClass());
concreteButtonInstance.addClass("vjs-airplay");
var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(
new concreteButtonClass()
);
concreteButtonInstance.addClass('vjs-airplay');
}
});
}

View File

@@ -1,5 +1,5 @@
import { h } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { SOCIAL_PLATFORMS } from '../utils/social.js';
import { classNames } from '../utils/helpers.js';

View File

@@ -4,6 +4,7 @@ import {
CHAT_PLACEHOLDER_OFFLINE,
} from './constants.js';
import showdown from '/js/web_modules/showdown.js';
export function formatMessageText(message, username) {
showdown.setFlavor('github');
let formattedText = new showdown.Converter({
@@ -278,3 +279,18 @@ export function convertOnPaste( event = { preventDefault() {} }) {
document.execCommand('insertText', false, value);
}
}
export function formatTimestamp(sentAt) {
sentAt = new Date(sentAt);
if (isNaN(sentAt)) {
return '';
}
let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000);
if (diffInDays >= 1) {
return `Sent at ${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` +
sentAt.toLocaleTimeString();
}
return `Sent at ${sentAt.toLocaleTimeString()}`;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,301 @@
/**
*
* The shadowDom / Intersection Observer version of Paul's concept:
* https://github.com/paulirish/lite-youtube-embed
*
* A lightweight YouTube embed. Still should feel the same to the user, just
* MUCH faster to initialize and paint.
*
* Thx to these as the inspiration
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
* https://autoplay-youtube-player.glitch.me/
*
* Once built it, I also found these (👍👍):
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube
* https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe
*/
class LiteYTEmbed extends HTMLElement {
constructor() {
super();
this.iframeLoaded = false;
this.setupDom();
}
static get observedAttributes() {
return ['videoid'];
}
connectedCallback() {
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {
once: true,
});
this.addEventListener('click', () => this.addIframe());
}
get videoId() {
return encodeURIComponent(this.getAttribute('videoid') || '');
}
set videoId(id) {
this.setAttribute('videoid', id);
}
get videoTitle() {
return this.getAttribute('videotitle') || 'Video';
}
set videoTitle(title) {
this.setAttribute('videotitle', title);
}
get videoPlay() {
return this.getAttribute('videoPlay') || 'Play';
}
set videoPlay(name) {
this.setAttribute('videoPlay', name);
}
get videoStartAt() {
return Number(this.getAttribute('videoStartAt') || '0');
}
set videoStartAt(time) {
this.setAttribute('videoStartAt', String(time));
}
get autoLoad() {
return this.hasAttribute('autoload');
}
set autoLoad(value) {
if (value) {
this.setAttribute('autoload', '');
}
else {
this.removeAttribute('autoload');
}
}
get params() {
return `start=${this.videoStartAt}&${this.getAttribute('params')}`;
}
/**
* Define our shadowDOM for the component
*/
setupDom() {
const shadowDom = this.attachShadow({ mode: 'open' });
shadowDom.innerHTML = `
<style>
:host {
contain: content;
display: block;
position: relative;
width: 100%;
padding-bottom: calc(100% / (16 / 9));
}
#frame, #fallbackPlaceholder, iframe {
position: absolute;
width: 100%;
height: 100%;
}
#frame {
cursor: pointer;
}
#fallbackPlaceholder {
object-fit: cover;
}
#frame::before {
content: '';
display: block;
position: absolute;
top: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
background-position: top;
background-repeat: repeat-x;
height: 60px;
padding-bottom: 50px;
width: 100%;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
z-index: 1;
}
/* play button */
.lty-playbtn {
width: 70px;
height: 46px;
background-color: #212121;
z-index: 1;
opacity: 0.8;
border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
border: 0;
}
#frame:hover .lty-playbtn {
background-color: #f00;
opacity: 1;
}
/* play button triangle */
.lty-playbtn:before {
content: '';
border-style: solid;
border-width: 11px 0 11px 19px;
border-color: transparent transparent transparent #fff;
}
.lty-playbtn,
.lty-playbtn:before {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
/* Post-click styles */
.lyt-activated {
cursor: unset;
}
#frame.lyt-activated::before,
.lyt-activated .lty-playbtn {
display: none;
}
</style>
<div id="frame">
<picture>
<source id="webpPlaceholder" type="image/webp">
<source id="jpegPlaceholder" type="image/jpeg">
<img id="fallbackPlaceholder" referrerpolicy="origin">
</picture>
<button class="lty-playbtn"></button>
</div>
`;
this.domRefFrame = this.shadowRoot.querySelector('#frame');
this.domRefImg = {
fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'),
webp: this.shadowRoot.querySelector('#webpPlaceholder'),
jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'),
};
this.domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn');
}
/**
* Parse our attributes and fire up some placeholders
*/
setupComponent() {
this.initImagePlaceholder();
this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);
if (this.autoLoad) {
this.initIntersectionObserver();
}
}
/**
* Lifecycle method that we use to listen for attribute changes to period
* @param {*} name
* @param {*} oldVal
* @param {*} newVal
*/
attributeChangedCallback(name, oldVal, newVal) {
switch (name) {
case 'videoid': {
if (oldVal !== newVal) {
this.setupComponent();
// if we have a previous iframe, remove it and the activated class
if (this.domRefFrame.classList.contains('lyt-activated')) {
this.domRefFrame.classList.remove('lyt-activated');
this.shadowRoot.querySelector('iframe').remove();
}
}
break;
}
}
}
/**
* Inject the iframe into the component body
*/
addIframe() {
if (!this.iframeLoaded) {
const iframeHTML = `
<iframe frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
src="https://www.youtube.com/embed/${this.videoId}?autoplay=1&${this.params}"
></iframe>`;
this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);
this.domRefFrame.classList.add('lyt-activated');
this.iframeLoaded = true;
}
}
/**
* Setup the placeholder image for the component
*/
initImagePlaceholder() {
// we don't know which image type to preload, so warm the connection
LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/');
const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`;
const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
this.domRefImg.webp.srcset = posterUrlWebp;
this.domRefImg.jpeg.srcset = posterUrlJpeg;
this.domRefImg.fallback.src = posterUrlJpeg;
this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
this.domRefImg.fallback.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`);
}
/**
* Setup the Intersection Observer to load the iframe when scrolled into view
*/
initIntersectionObserver() {
if ('IntersectionObserver' in window &&
'IntersectionObserverEntry' in window) {
const options = {
root: null,
rootMargin: '0px',
threshold: 0,
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.iframeLoaded) {
LiteYTEmbed.warmConnections();
this.addIframe();
observer.unobserve(this);
}
});
}, options);
observer.observe(this);
}
}
/**
* Add a <link rel={preload | preconnect} ...> to the head
* @param {*} kind
* @param {*} url
* @param {*} as
*/
static addPrefetch(kind, url, as) {
const linkElem = document.createElement('link');
linkElem.rel = kind;
linkElem.href = url;
if (as) {
linkElem.as = as;
}
linkElem.crossOrigin = 'true';
document.head.append(linkElem);
}
/**
* Begin preconnecting to warm up the iframe load Since the embed's netwok
* requests load within its iframe, preload/prefetch'ing them outside the
* iframe will only cause double-downloads. So, the best we can do is warm up
* a few connections to origins that are in the critical path.
*
* Maybe `<link rel=preload as=document>` would work, but it's unsupported:
* http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site
* Isolation and split caches adding serious complexity.
*/
static warmConnections() {
if (LiteYTEmbed.preconnected)
return;
// Host that YT uses to serve JS needed by player, per amp-youtube
LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com');
// The iframe document and most of its subresources come right off
// youtube.com
LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com');
// The botguard script is fetched off from google.com
LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
// TODO: Not certain if these ad related domains are in the critical path.
// Could verify with domain-specific throttling.
LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
LiteYTEmbed.preconnected = true;
}
}
LiteYTEmbed.preconnected = false;
// Register custom element
customElements.define('lite-youtube', LiteYTEmbed);
export { LiteYTEmbed };

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,116 @@
.vjs-theme-fantasy {
--vjs-theme-fantasy--primary: #9f44b4;
--vjs-theme-fantasy--secondary: #fff;
}
.vjs-theme-fantasy .vjs-big-play-button {
width: 70px;
height: 70px;
background: none;
line-height: 70px;
font-size: 80px;
border: none;
top: 50%;
left: 50%;
margin-top: -35px;
margin-left: -35px;
color: var(--vjs-theme-fantasy--primary);
}
.vjs-theme-fantasy:hover .vjs-big-play-button,
.vjs-theme-fantasy.vjs-big-play-button:focus {
background-color: transparent;
color: #fff;
}
.vjs-theme-fantasy .vjs-control-bar {
height: 54px;
}
.vjs-theme-fantasy .vjs-button > .vjs-icon-placeholder::before {
line-height: 54px;
}
.vjs-theme-fantasy .vjs-time-control {
line-height: 54px;
}
/* Play Button */
.vjs-theme-fantasy .vjs-play-control {
font-size: 1.5em;
position: relative;
}
.vjs-theme-fantasy .vjs-volume-panel {
order: 4;
}
.vjs-theme-fantasy .vjs-volume-bar {
margin-top: 2.5em;
}
.vjs-theme-city .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal {
height: 100%;
}
.vjs-theme-fantasy .vjs-progress-control .vjs-progress-holder {
font-size: 1.5em;
}
.vjs-theme-fantasy .vjs-progress-control:hover .vjs-progress-holder {
font-size: 1.5em;
}
.vjs-theme-fantasy .vjs-play-control .vjs-icon-placeholder::before {
height: 1.3em;
width: 1.3em;
margin-top: 0.2em;
border-radius: 1em;
border: 3px solid var(--vjs-theme-fantasy--secondary);
top: 2px;
left: 9px;
line-height: 1.1;
}
.vjs-theme-fantasy .vjs-play-control:hover .vjs-icon-placeholder::before {
border: 3px solid var(--vjs-theme-fantasy--secondary);
}
.vjs-theme-fantasy .vjs-play-progress {
background-color: var(--vjs-theme-fantasy--primary);
}
.vjs-theme-fantasy .vjs-play-progress::before {
height: 0.8em;
width: 0.8em;
content: '';
background-color: var(--vjs-theme-fantasy--primary);
border: 4px solid var(--vjs-theme-fantasy--secondary);
border-radius: 0.8em;
top: -0.25em;
}
.vjs-theme-fantasy .vjs-progress-control {
font-size: 14px;
}
.vjs-theme-fantasy .vjs-fullscreen-control {
order: 6;
}
.vjs-theme-fantasy .vjs-remaining-time {
display: none;
}
/* Nyan version */
.vjs-theme-fantasy.nyan .vjs-play-progress {
background: linear-gradient(to bottom, #fe0000 0%, #fe9a01 16.666666667%, #fe9a01 16.666666667%, #ffff00 33.332666667%, #ffff00 33.332666667%, #32ff00 49.999326667%, #32ff00 49.999326667%, #0099fe 66.6659926%, #0099fe 66.6659926%, #6633ff 83.33266%, #6633ff 83.33266%);
}
.vjs-theme-fantasy.nyan .vjs-play-progress::before {
height: 1.3em;
width: 1.3em;
background: svg-load('icons/nyan-cat.svg', fill=#fff) no-repeat;
border: none;
top: -0.35em;
}

View File

@@ -0,0 +1,25 @@
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function createCommonjsModule(fn, basedir, module) {
return module = {
path: basedir,
exports: {},
require: function (path, base) {
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
}
}, fn(module, module.exports), module.exports;
}
function getDefaultExportFromNamespaceIfNotNamed (n) {
return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n;
}
function commonjsRequire () {
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
}
export { commonjsGlobal as a, getDefaultExportFromNamespaceIfNotNamed as b, createCommonjsModule as c, getDefaultExportFromCjs as g };

View File

@@ -0,0 +1,44 @@
import { b as getDefaultExportFromNamespaceIfNotNamed, a as commonjsGlobal } from './_commonjsHelpers-37fa8da4.js';
var _nodeResolve_empty = {};
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
'default': _nodeResolve_empty
});
var minDoc = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(_nodeResolve_empty$1);
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
typeof window !== 'undefined' ? window : {};
var doccy;
if (typeof document !== 'undefined') {
doccy = document;
} else {
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
if (!doccy) {
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
}
}
var document_1 = doccy;
var win;
if (typeof window !== "undefined") {
win = window;
} else if (typeof commonjsGlobal !== "undefined") {
win = commonjsGlobal;
} else if (typeof self !== "undefined"){
win = self;
} else {
win = {};
}
var window_1 = win;
export { document_1 as d, window_1 as w };

3
webroot/js/web_modules/htm.js vendored Normal file
View File

@@ -0,0 +1,3 @@
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a);}return e},t=new Map;function htm_module(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
export default htm_module;

14
webroot/js/web_modules/import-map.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"imports": {
"@joeattardi/emoji-button": "./@joeattardi/emoji-button.js",
"@justinribeiro/lite-youtube": "./@justinribeiro/lite-youtube.js",
"@videojs/http-streaming/dist/videojs-http-streaming.min.js": "./@videojs/http-streaming/dist/videojs-http-streaming.min.js",
"@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css",
"htm": "./htm.js",
"preact": "./preact.js",
"showdown": "./showdown.js",
"tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css",
"video.js/dist/video-js.min.css": "./videojs/dist/video-js.min.css",
"video.js/dist/video.min.js": "./videojs/dist/video.min.js"
}
}

3
webroot/js/web_modules/preact.js vendored Normal file

File diff suppressed because one or more lines are too long

5044
webroot/js/web_modules/showdown.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -14,6 +14,7 @@ May have overrides for other components with own stylesheets.
html {
font-size: 14px;
scrollbar-width: none;
}
a:hover {
@@ -24,6 +25,9 @@ a:hover {
width: 0px;
background: transparent;
}
.scrollbar-hidden {
scrollbar-width: none; /* moz only */
}
#app-container * {

View File

@@ -80,6 +80,7 @@
.emoji-picker.owncast {
--secondary-text-color: rgba(255,255,255,.5);
--category-button-color: rgba(255,255,255,.5);
--hover-color: rgba(255,255,255,.25);
background: rgba(26,32,44,1); /* tailwind bg-gray-900 */
color: rgba(226,232,240,1); /* tailwind text-gray-300 */
@@ -101,15 +102,19 @@
}
.emoji-picker__emojis::-webkit-scrollbar-track {
border-radius: 8px;
background-color: rgba(0,0,0,.2);
background-color: black;
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
}
.emoji-picker__emojis::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,.45);
background-color: var(--category-button-color);
border-radius: 8px;
}
.emoji-picker__emojis {
scrollbar-color: var(--category-button-color) black;
}
/******************************/