20 changed files with 316 additions and 594 deletions
@ -1,64 +0,0 @@
@@ -1,64 +0,0 @@
|
||||
// DELETE THIS FILE LATER.
|
||||
|
||||
Vue.component('owncast-footer', { |
||||
props: { |
||||
appVersion: { |
||||
type: String, |
||||
default: '0.1', |
||||
}, |
||||
}, |
||||
|
||||
template: ` |
||||
<footer class="flex"> |
||||
<span> |
||||
<a href="${URL_OWNCAST}" target="_blank">About Owncast</a> |
||||
</span> |
||||
<span>Version {{appVersion}}</span> |
||||
</footer> |
||||
`,
|
||||
}); |
||||
|
||||
|
||||
Vue.component('stream-tags', { |
||||
props: ['tags'], |
||||
template: ` |
||||
<ul |
||||
class="tag-list flex" |
||||
v-if="this.tags.length" |
||||
> |
||||
<li class="tag rounded-sm text-gray-100 bg-gray-700" |
||||
v-for="tag in this.tags" |
||||
v-bind:key="tag" |
||||
> |
||||
{{tag}} |
||||
</li> |
||||
</ul> |
||||
`,
|
||||
}); |
||||
|
||||
Vue.component('user-details', { |
||||
props: ['logo', 'platforms', 'summary', 'tags'], |
||||
template: ` |
||||
<div class="user-content"> |
||||
<div |
||||
class="user-image rounded-full bg-white" |
||||
v-bind:style="{ backgroundImage: 'url(' + logo + ')' }" |
||||
> |
||||
<img |
||||
class="logo visually-hidden" |
||||
alt="Logo" |
||||
v-bind:src="logo"> |
||||
</div> |
||||
<div class="user-content-header border-b border-gray-500 border-solid"> |
||||
<h2 class="font-semibold"> |
||||
About <span class="streamer-name text-indigo-600"> |
||||
<slot></slot> |
||||
</span> |
||||
</h2> |
||||
<social-list v-bind:platforms="platforms"></social-list> |
||||
<div class="stream-summary" v-html="summary"></div> |
||||
<stream-tags v-bind:tags="tags"></stream-tags> |
||||
</div> |
||||
</div> |
||||
`,
|
||||
}); |
@ -1,9 +1,9 @@
@@ -1,9 +1,9 @@
|
||||
import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module"; |
||||
|
||||
import { messageBubbleColorForString } from '../utils/user-colors.js'; |
||||
import { formatMessageText } from '../utils/chat.js'; |
||||
import { generateAvatar } from '../utils.js'; |
||||
import SOCKET_MESSAGE_TYPES from '../utils/socket-message-types.js'; |
||||
import { messageBubbleColorForString } from '../../utils/user-colors.js'; |
||||
import { formatMessageText } from '../../utils/chat.js'; |
||||
import { generateAvatar } from '../../utils/helpers.js'; |
||||
import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js'; |
||||
|
||||
export default class Message extends Component { |
||||
render(props) { |
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
import { html } from "https://unpkg.com/htm/preact/index.mjs?module"; |
||||
import { SOCIAL_PLATFORMS } from './utils/social.js'; |
||||
import { classNames } from './utils.js'; |
||||
import { SOCIAL_PLATFORMS } from '../utils/social.js'; |
||||
import { classNames } from '../utils/helpers.js'; |
||||
|
||||
export default function SocialIcon(props) { |
||||
const { platform, url } = props; |
@ -1,440 +0,0 @@
@@ -1,440 +0,0 @@
|
||||
// DELETE THIS FILE LATER.
|
||||
|
||||
import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js'; |
||||
|
||||
const KEY_USERNAME = 'owncast_username'; |
||||
const KEY_AVATAR = 'owncast_avatar'; |
||||
const KEY_CHAT_DISPLAYED = 'owncast_chat'; |
||||
const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; |
||||
const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; |
||||
const CHAT_PLACEHOLDER_TEXT = 'Message'; |
||||
const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; |
||||
|
||||
class Message { |
||||
constructor(model) { |
||||
this.author = model.author; |
||||
this.body = model.body; |
||||
this.image = model.image || generateAvatar(model.author); |
||||
this.id = model.id; |
||||
this.type = model.type; |
||||
} |
||||
|
||||
formatText() { |
||||
showdown.setFlavor('github'); |
||||
let formattedText = new showdown.Converter({ |
||||
emoji: true, |
||||
openLinksInNewWindow: true, |
||||
tables: false, |
||||
simplifiedAutoLink: false, |
||||
literalMidWordUnderscores: true, |
||||
strikethrough: true, |
||||
ghMentions: false, |
||||
}).makeHtml(this.body); |
||||
|
||||
formattedText = this.linkify(formattedText, this.body); |
||||
formattedText = this.highlightUsername(formattedText); |
||||
|
||||
return addNewlines(formattedText); |
||||
} |
||||
|
||||
// TODO: Move this into a util function once we can organize code
|
||||
// and split things up.
|
||||
linkify(text, rawText) { |
||||
const urls = getURLs(stripTags(rawText)); |
||||
if (urls) { |
||||
urls.forEach(function (url) { |
||||
let linkURL = url; |
||||
|
||||
// Add http prefix if none exist in the URL so it actually
|
||||
// will work in an anchor tag.
|
||||
if (linkURL.indexOf('http') === -1) { |
||||
linkURL = 'http://' + linkURL; |
||||
} |
||||
|
||||
// Remove the protocol prefix in the display URLs just to make
|
||||
// things look a little nicer.
|
||||
const displayURL = url.replace(/(^\w+:|^)\/\//, ''); |
||||
const link = `<a href="${linkURL}" target="_blank">${displayURL}</a>`; |
||||
text = text.replace(url, link); |
||||
|
||||
if (getYoutubeIdFromURL(url)) { |
||||
if (this.isTextJustURLs(text, [url, displayURL])) { |
||||
text = ''; |
||||
} else { |
||||
text += '<br/>'; |
||||
} |
||||
|
||||
const youtubeID = getYoutubeIdFromURL(url); |
||||
text += getYoutubeEmbedFromID(youtubeID); |
||||
} else if (url.indexOf('instagram.com/p/') > -1) { |
||||
if (this.isTextJustURLs(text, [url, displayURL])) { |
||||
text = ''; |
||||
} else { |
||||
text += `<br/>`; |
||||
} |
||||
text += getInstagramEmbedFromURL(url); |
||||
} else if (isImage(url)) { |
||||
if (this.isTextJustURLs(text, [url, displayURL])) { |
||||
text = ''; |
||||
} else { |
||||
text += `<br/>`; |
||||
} |
||||
text += getImageForURL(url); |
||||
} |
||||
}.bind(this)); |
||||
} |
||||
return text; |
||||
} |
||||
|
||||
isTextJustURLs(text, urls) { |
||||
for (var i = 0; i < urls.length; i++) { |
||||
const url = urls[i]; |
||||
if (stripTags(text) === url) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
userColor() { |
||||
return messageBubbleColorForString(this.author); |
||||
} |
||||
|
||||
highlightUsername(message) { |
||||
const username = document.getElementById('self-message-author').value; |
||||
const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); |
||||
return message.replace(pattern, '<span class="highlighted">$&</span>'); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
class MessagingInterface { |
||||
constructor() { |
||||
this.chatDisplayed = false; |
||||
this.username = ''; |
||||
this.messageCharCount = 0; |
||||
this.maxMessageLength = 500; |
||||
this.maxMessageBuffer = 20; |
||||
this.chatUsernames = []; |
||||
|
||||
this.onReceivedMessages = this.onReceivedMessages.bind(this); |
||||
this.disableChat = this.disableChat.bind(this); |
||||
this.enableChat = this.enableChat.bind(this); |
||||
} |
||||
|
||||
init() { |
||||
this.tagAppContainer = document.getElementById('app-container'); |
||||
this.tagChatToggle = document.getElementById('chat-toggle'); |
||||
this.tagUserInfoChanger = document.getElementById('user-info-change'); |
||||
this.tagUsernameDisplay = document.getElementById('username-display'); |
||||
this.tagMessageFormWarning = document.getElementById('message-form-warning'); |
||||
|
||||
this.inputMessageAuthor = document.getElementById('self-message-author'); |
||||
this.inputChangeUserName = document.getElementById('username-change-input'); |
||||
|
||||
this.btnUpdateUserName = document.getElementById('button-update-username'); |
||||
this.btnCancelUpdateUsername = document.getElementById('button-cancel-change'); |
||||
this.btnSubmitMessage = document.getElementById('button-submit-message'); |
||||
|
||||
this.formMessageInput = document.getElementById('message-body-form'); |
||||
|
||||
this.imgUsernameAvatar = document.getElementById('username-avatar'); |
||||
this.textUserInfoDisplay = document.getElementById('user-info-display'); |
||||
|
||||
this.scrollableMessagesContainer = document.getElementById('messages-container'); |
||||
|
||||
// add events
|
||||
this.tagChatToggle.addEventListener('click', this.handleChatToggle.bind(this)); |
||||
this.textUserInfoDisplay.addEventListener('click', this.handleShowChangeNameForm.bind(this)); |
||||
|
||||
this.btnUpdateUserName.addEventListener('click', this.handleUpdateUsername.bind(this)); |
||||
this.btnCancelUpdateUsername.addEventListener('click', this.handleHideChangeNameForm.bind(this)); |
||||
|
||||
this.inputChangeUserName.addEventListener('keydown', this.handleUsernameKeydown.bind(this)); |
||||
this.formMessageInput.addEventListener('keydown', this.handleMessageInputKeydown.bind(this)); |
||||
this.formMessageInput.addEventListener('keyup', this.handleMessageInputKeyup.bind(this)); |
||||
this.formMessageInput.addEventListener('blur', this.handleMessageInputBlur.bind(this)); |
||||
this.btnSubmitMessage.addEventListener('click', this.handleSubmitChatButton.bind(this)); |
||||
|
||||
this.initLocalStates(); |
||||
|
||||
if (hasTouchScreen()) { |
||||
setVHvar(); |
||||
window.addEventListener("orientationchange", setVHvar); |
||||
this.tagAppContainer.classList.add('touch-screen'); |
||||
} |
||||
} |
||||
|
||||
initLocalStates() { |
||||
this.username = getLocalStorage(KEY_USERNAME) || generateUsername(); |
||||
this.imgUsernameAvatar.src = |
||||
getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`); |
||||
this.updateUsernameFields(this.username); |
||||
|
||||
this.chatDisplayed = getLocalStorage(KEY_CHAT_DISPLAYED) || true; |
||||
this.displayChat(); |
||||
this.disableChat(); // Disabled by default.
|
||||
} |
||||
|
||||
updateUsernameFields(username) { |
||||
this.tagUsernameDisplay.innerText = username; |
||||
this.inputChangeUserName.value = username; |
||||
this.inputMessageAuthor.value = username; |
||||
} |
||||
|
||||
displayChat() { |
||||
if (this.chatDisplayed) { |
||||
this.tagAppContainer.classList.add('chat'); |
||||
this.tagAppContainer.classList.remove('no-chat'); |
||||
jumpToBottom(this.scrollableMessagesContainer); |
||||
} else { |
||||
this.tagAppContainer.classList.add('no-chat'); |
||||
this.tagAppContainer.classList.remove('chat'); |
||||
} |
||||
this.setChatPlaceholderText(); |
||||
} |
||||
|
||||
|
||||
handleChatToggle() { |
||||
this.chatDisplayed = !this.chatDisplayed; |
||||
if (this.chatDisplayed) { |
||||
setLocalStorage(KEY_CHAT_DISPLAYED, this.chatDisplayed); |
||||
} else { |
||||
clearLocalStorage(KEY_CHAT_DISPLAYED); |
||||
} |
||||
this.displayChat(); |
||||
} |
||||
|
||||
handleShowChangeNameForm() { |
||||
this.textUserInfoDisplay.style.display = 'none'; |
||||
this.tagUserInfoChanger.style.display = 'flex'; |
||||
if (document.body.clientWidth < 640) { |
||||
this.tagChatToggle.style.display = 'none'; |
||||
} |
||||
} |
||||
|
||||
handleHideChangeNameForm() { |
||||
this.textUserInfoDisplay.style.display = 'flex'; |
||||
this.tagUserInfoChanger.style.display = 'none'; |
||||
if (document.body.clientWidth < 640) { |
||||
this.tagChatToggle.style.display = 'inline-block'; |
||||
} |
||||
} |
||||
|
||||
handleUpdateUsername() { |
||||
const oldName = this.username; |
||||
var newValue = this.inputChangeUserName.value; |
||||
newValue = newValue.trim(); |
||||
// do other string cleanup?
|
||||
|
||||
if (newValue) { |
||||
this.username = newValue; |
||||
this.updateUsernameFields(newValue); |
||||
this.imgUsernameAvatar.src = generateAvatar(`${newValue}${Date.now()}`); |
||||
setLocalStorage(KEY_USERNAME, newValue); |
||||
setLocalStorage(KEY_AVATAR, this.imgUsernameAvatar.src); |
||||
} |
||||
this.handleHideChangeNameForm(); |
||||
|
||||
if (oldName !== newValue) { |
||||
this.sendUsernameChange(oldName, newValue, this.imgUsernameAvatar.src); |
||||
} |
||||
} |
||||
|
||||
handleUsernameKeydown(event) { |
||||
if (event.keyCode === 13) { // enter
|
||||
this.handleUpdateUsername(); |
||||
} else if (event.keyCode === 27) { // esc
|
||||
this.handleHideChangeNameForm(); |
||||
} |
||||
} |
||||
|
||||
sendUsernameChange(oldName, newName, image) { |
||||
const nameChange = { |
||||
type: SOCKET_MESSAGE_TYPES.NAME_CHANGE, |
||||
oldName: oldName, |
||||
newName: newName, |
||||
image: image, |
||||
}; |
||||
|
||||
this.send(nameChange); |
||||
} |
||||
|
||||
tryToComplete() { |
||||
const rawValue = this.formMessageInput.innerHTML; |
||||
const position = getCaretPosition(this.formMessageInput); |
||||
const at = rawValue.lastIndexOf('@', position - 1); |
||||
|
||||
if (at === -1) { |
||||
return false; |
||||
} |
||||
|
||||
var partial = rawValue.substring(at + 1, position).trim(); |
||||
|
||||
if (partial === this.suggestion) { |
||||
partial = this.partial; |
||||
} else { |
||||
this.partial = partial; |
||||
} |
||||
|
||||
const possibilities = this.chatUsernames.filter(function (username) { |
||||
return username.toLowerCase().startsWith(partial.toLowerCase()); |
||||
}); |
||||
|
||||
if (this.completionIndex === undefined || ++this.completionIndex >= possibilities.length) { |
||||
this.completionIndex = 0; |
||||
} |
||||
|
||||
if (possibilities.length > 0) { |
||||
this.suggestion = possibilities[this.completionIndex]; |
||||
|
||||
// TODO: Fix the space not working. I'm guessing because the DOM ignores spaces and it requires a nbsp or something?
|
||||
this.formMessageInput.innerHTML = rawValue.substring(0, at + 1) + this.suggestion + ' ' + rawValue.substring(position); |
||||
setCaretPosition(this.formMessageInput, at + this.suggestion.length + 2); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
handleMessageInputKeydown(event) { |
||||
var okCodes = [37,38,39,40,16,91,18,46,8]; |
||||
var value = this.formMessageInput.innerHTML.trim(); |
||||
var numCharsLeft = this.maxMessageLength - value.length; |
||||
if (event.keyCode === 13) { // enter
|
||||
if (!this.prepNewLine) { |
||||
this.submitChat(value); |
||||
event.preventDefault(); |
||||
this.prepNewLine = false; |
||||
|
||||
return; |
||||
} |
||||
} |
||||
if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift
|
||||
this.prepNewLine = true; |
||||
} |
||||
if (event.keyCode === 9) { // tab
|
||||
if (this.tryToComplete()) { |
||||
event.preventDefault(); |
||||
|
||||
// value could have been changed, update variables
|
||||
value = this.formMessageInput.innerHTML.trim(); |
||||
numCharsLeft = this.maxMessageLength - value.length; |
||||
} |
||||
} |
||||
|
||||
if (numCharsLeft <= this.maxMessageBuffer) { |
||||
this.tagMessageFormWarning.innerText = `${numCharsLeft} chars left`; |
||||
if (numCharsLeft <= 0 && !okCodes.includes(event.keyCode)) { |
||||
event.preventDefault(); |
||||
return; |
||||
} |
||||
} else { |
||||
this.tagMessageFormWarning.innerText = ''; |
||||
} |
||||
} |
||||
|
||||
handleMessageInputKeyup(event) { |
||||
if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift
|
||||
this.prepNewLine = false; |
||||
} |
||||
} |
||||
|
||||
handleMessageInputBlur(event) { |
||||
this.prepNewLine = false; |
||||
} |
||||
|
||||
handleSubmitChatButton(event) { |
||||
var value = this.formMessageInput.innerHTML.trim(); |
||||
if (value) { |
||||
this.submitChat(value); |
||||
event.preventDefault(); |
||||
return false; |
||||
} |
||||
event.preventDefault(); |
||||
return false; |
||||
} |
||||
|
||||
submitChat(content) { |
||||
if (!content) { |
||||
return; |
||||
} |
||||
var message = new Message({ |
||||
body: content, |
||||
author: this.username, |
||||
image: this.imgUsernameAvatar.src, |
||||
type: SOCKET_MESSAGE_TYPES.CHAT, |
||||
}); |
||||
this.send(message); |
||||
|
||||
// clear out things.
|
||||
this.formMessageInput.innerHTML = ''; |
||||
this.tagMessageFormWarning.innerText = ''; |
||||
|
||||
const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); |
||||
if (!hasSentFirstChatMessage) { |
||||
setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true); |
||||
this.setChatPlaceholderText(); |
||||
} |
||||
} |
||||
|
||||
disableChat() { |
||||
if (this.formMessageInput) { |
||||
this.formMessageInput.contentEditable = false; |
||||
this.formMessageInput.innerHTML = ''; |
||||
this.formMessageInput.setAttribute("placeholder", CHAT_PLACEHOLDER_OFFLINE); |
||||
} |
||||
} |
||||
|
||||
enableChat() { |
||||
if (this.formMessageInput) { |
||||
this.formMessageInput.contentEditable = true; |
||||
this.setChatPlaceholderText(); |
||||
} |
||||
} |
||||
|
||||
setChatPlaceholderText() { |
||||
// NOTE: This is a fake placeholder that is being styled via CSS.
|
||||
// You can't just set the .placeholder property because it's not a form element.
|
||||
const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); |
||||
const placeholderText = hasSentFirstChatMessage ? CHAT_PLACEHOLDER_TEXT : CHAT_INITIAL_PLACEHOLDER_TEXT; |
||||
this.formMessageInput.setAttribute("placeholder", placeholderText); |
||||
} |
||||
|
||||
// handle Vue.js message display
|
||||
onReceivedMessages(newMessages, oldMessages) { |
||||
// update the list of chat usernames
|
||||
newMessages.slice(oldMessages.length).forEach(function (message) { |
||||
var username; |
||||
|
||||
switch (message.type) { |
||||
case SOCKET_MESSAGE_TYPES.CHAT: |
||||
username = message.author; |
||||
break; |
||||
|
||||
case SOCKET_MESSAGE_TYPES.NAME_CHANGE: |
||||
username = message.newName; |
||||
break; |
||||
|
||||
default: |
||||
return; |
||||
} |
||||
|
||||
if (!this.chatUsernames.includes(username)) { |
||||
this.chatUsernames.push(username); |
||||
} |
||||
}, this); |
||||
|
||||
if (newMessages.length !== oldMessages.length) { |
||||
// jump to bottom
|
||||
jumpToBottom(this.scrollableMessagesContainer); |
||||
} |
||||
} |
||||
|
||||
send(messageJSON) { |
||||
console.error('MessagingInterface send() is not linked to the websocket component.'); |
||||
} |
||||
} |
||||
|
||||
export { Message, MessagingInterface } |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
// misc constants used throughout the app
|
||||
|
||||
export const URL_STATUS = `/status`; |
||||
export const URL_CHAT_HISTORY = `/chat`; |
||||
export const URL_CUSTOM_EMOJIS = `/emoji`; |
||||
export const URL_CONFIG = `/config`; |
||||
|
||||
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
||||
export const URL_STREAM = `/hls/stream.m3u8`; |
||||
export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; |
||||
|
||||
export const TIMER_STATUS_UPDATE = 5000; // ms
|
||||
export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins
|
||||
export const TIMER_STREAM_DURATION_COUNTER = 1000; |
||||
export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; |
||||
|
||||
export const MESSAGE_OFFLINE = 'Stream is offline.'; |
||||
export const MESSAGE_ONLINE = 'Stream is online'; |
||||
|
||||
export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer
|
||||
|
||||
|
||||
export const KEY_USERNAME = 'owncast_username'; |
||||
export const KEY_AVATAR = 'owncast_avatar'; |
||||
export const KEY_CHAT_DISPLAYED = 'owncast_chat'; |
||||
export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; |
||||
export const CHAT_INITIAL_PLACEHOLDER_TEXT = 'Type here to chat, no account necessary.'; |
||||
export const CHAT_PLACEHOLDER_TEXT = 'Message'; |
||||
export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; |
@ -1,22 +1,3 @@
@@ -1,22 +1,3 @@
|
||||
export const URL_STATUS = `/status`; |
||||
export const URL_CHAT_HISTORY = `/chat`; |
||||
export const URL_CUSTOM_EMOJIS = `/emoji`; |
||||
export const URL_CONFIG = `/config`; |
||||
|
||||
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
||||
export const URL_STREAM = `/hls/stream.m3u8`; |
||||
export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/entry`; |
||||
|
||||
export const TIMER_STATUS_UPDATE = 5000; // ms
|
||||
export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins
|
||||
export const TIMER_STREAM_DURATION_COUNTER = 1000; |
||||
export const TEMP_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; |
||||
|
||||
export const MESSAGE_OFFLINE = 'Stream is offline.'; |
||||
export const MESSAGE_ONLINE = 'Stream is online'; |
||||
|
||||
export const URL_OWNCAST = 'https://github.com/gabek/owncast'; // used in footer
|
||||
|
||||
export function getLocalStorage(key) { |
||||
try { |
||||
return localStorage.getItem(key); |
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
/** |
||||
* These are the types of messages that we can handle with the websocket. |
||||
* Mostly used by `websocket.js` but if other components need to handle |
||||
* different types then it can import this file. |
||||
*/ |
||||
export default { |
||||
CHAT: 'CHAT', |
||||
PING: 'PING', |
||||
NAME_CHANGE: 'NAME_CHANGE', |
||||
PONG: 'PONG' |
||||
}; |
@ -0,0 +1,172 @@
@@ -0,0 +1,172 @@
|
||||
/* |
||||
Spefici styles for app layout |
||||
|
||||
*/ |
||||
|
||||
/* variables */ |
||||
:root { |
||||
--header-height: 3.5em; |
||||
--right-col-width: 24em; |
||||
--video-container-height: 55vh; |
||||
--header-bg-color: rgba(20,0,40,1); |
||||
--user-image-width: 10em; |
||||
} |
||||
|
||||
html { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
a:hover { |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
::-webkit-scrollbar { |
||||
width: 0px; |
||||
background: transparent; |
||||
} |
||||
|
||||
.visually-hidden { |
||||
position: absolute !important; |
||||
height: 1px; |
||||
width: 1px; |
||||
overflow: hidden; |
||||
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ |
||||
clip: rect(1px, 1px, 1px, 1px); |
||||
white-space: nowrap; /* added line */ |
||||
} |
||||
|
||||
|
||||
header { |
||||
height: var(--header-height); |
||||
background-color: var(--header-bg-color); |
||||
} |
||||
|
||||
#logo-container { |
||||
background-size: 1.35em; |
||||
} |
||||
|
||||
#chat-toggle { |
||||
min-width: 3rem; |
||||
} |
||||
|
||||
|
||||
#user-info-change { |
||||
display: none; |
||||
} |
||||
|
||||
|
||||
#stream-info span { |
||||
font-size: .70rem; |
||||
} |
||||
|
||||
|
||||
/* ************************************************ */ |
||||
|
||||
#video-container { |
||||
height: calc(var(--video-container-height)); |
||||
margin-top: var(--header-height); |
||||
background-size: 30%; |
||||
} |
||||
#video-container #video { |
||||
transition: opacity .5s; |
||||
opacity: 0; |
||||
pointer-events: none; |
||||
} |
||||
.online #video { |
||||
opacity: 1; |
||||
pointer-events: auto; |
||||
} |
||||
|
||||
/* *********** overrides when chat is off ***************************** */ |
||||
|
||||
|
||||
.no-chat footer { |
||||
justify-content: center; |
||||
} |
||||
|
||||
.no-chat #chat-toggle { |
||||
opacity: .75; |
||||
} |
||||
|
||||
|
||||
/* *********** overrides when chat is on ***************************** */ |
||||
|
||||
.chat #video-container, |
||||
.chat #stream-info, |
||||
.chat #user-content { |
||||
width: calc(100% - var(--right-col-width)); |
||||
} |
||||
|
||||
|
||||
|
||||
/* ************************************************8 */ |
||||
|
||||
|
||||
@media screen and (max-width: 860px) { |
||||
:root { |
||||
--right-col-width: 20em; |
||||
--user-image-width: 6em; |
||||
} |
||||
|
||||
#chat-container { |
||||
width: var(--right-col-width); |
||||
} |
||||
|
||||
} |
||||
|
||||
/* single col layout */ |
||||
@media screen and (max-width: 640px ) { |
||||
:root { |
||||
--right-col-width: 0; |
||||
--video-container-height: 40vh; |
||||
} |
||||
#logo-container { |
||||
display: none; |
||||
} |
||||
header h1 { |
||||
max-width: 58%; |
||||
} |
||||
#user-options-container { |
||||
max-width: 41%; |
||||
} |
||||
|
||||
#chat-container { |
||||
width: 100%; |
||||
position: static; |
||||
/* min-height: calc(100vh - var(--header-height)); */ |
||||
height: calc(100vh - var(--header-height) - var(--video-container-height) - 3vh) |
||||
} |
||||
#messages-container { |
||||
min-height: unset; |
||||
} |
||||
#user-content { |
||||
width: 100%; |
||||
} |
||||
#stream-info { |
||||
width: 100%; |
||||
} |
||||
#video-container { |
||||
width: 100%; |
||||
} |
||||
.chat #video-container { |
||||
width: 100%; |
||||
} |
||||
.chat #user-content { |
||||
display: none; |
||||
} |
||||
.chat footer { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-height: 860px ) { |
||||
:root { |
||||
--video-container-height: 40vh; |
||||
} |
||||
.user-content { |
||||
flex-direction: column; |
||||
} |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
video.video-js { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: block; |
||||
min-height: 100% |
||||
} |
||||
|
||||
.vjs-airplay .vjs-icon-placeholder::before { |
||||
content: url("../img/airplay.png"); |
||||
} |
||||
|
||||
|
||||
|
||||
/* position: relative; |
||||
width: 100%; |
||||
height: calc((9 / 16) * 100vw); |
||||
max-height: calc(100vh - 169px); |
||||
min-height: 480px; |
||||
background: #000; */ |
||||
|
||||
/* |
||||
YOUTUBE |
||||
style="--ytd-watch-flexy-scrollbar-width: 15px; --ytd-watch-flexy-panel-max-height: 460px; --ytd-watch-flexy-chat-max-height: 460px;" |
||||
|
||||
--ytd-watch-flexy-scrollbar-width: 15px; |
||||
--ytd-watch-flexy-panel-max-height: 460px; |
||||
--ytd-watch-flexy-chat-max-height: 460px; |
||||
|
||||
--ytd-watch-flexy-width-ratio: 16; |
||||
--ytd-watch-flexy-height-ratio: 9; |
||||
--ytd-watch-flexy-space-below-player: 136px; |
||||
|
||||
--ytd-watch-flexy-non-player-height: calc(var(--ytd-watch-flexy-masthead-height) + var(--ytd-margin-6x) + var(--ytd-watch-flexy-space-below-player)); |
||||
|
||||
--ytd-watch-flexy-non-player-width: calc(var(--ytd-watch-flexy-sidebar-width) + (3 * var(--ytd-margin-6x))); |
||||
|
||||
--ytd-watch-flexy-min-player-height: 240px; |
||||
|
||||
--ytd-watch-flexy-min-player-width: calc(var(--ytd-watch-flexy-min-player-height) * (var(--ytd-watch-flexy-width-ratio) / var(--ytd-watch-flexy-height-ratio))); |
||||
|
||||
--ytd-watch-flexy-max-player-height: calc(100vh - |
||||
(var(--ytd-watch-flexy-masthead-height) + var(--ytd-margin-6x) + var(--ytd-watch-flexy-space-below-player))); |
||||
|
||||
--ytd-watch-flexy-max-player-width: |
||||
calc((100vh - (var(--ytd-watch-flexy-masthead-height) + var(--ytd-margin-6x) + var(--ytd-watch-flexy-space-below-player))) * |
||||
(var(--ytd-watch-flexy-width-ratio) / var(--ytd-watch-flexy-height-ratio))); |
||||
|
||||
|
||||
|
||||
--ytd-watch-flexy-sidebar-width: 402px; |
||||
--ytd-watch-flexy-sidebar-min-width: 300px; |
||||
--ytd-watch-flexy-masthead-height: 56px; |
||||
min-width: 0; |
||||
|
||||
*/ |
Loading…
Reference in new issue