0

Merge branch 'web-layout'

* web-layout:
  Show max viewers
  Move to videojs and point to remote video on goth.land
  form functionailties
  progress. implement chat toggling
  fix msg container
  use app file from web-layout
  style message items
  Guard against the infinite that can take place when the ws server goes unavailable
  use css vars
  initial chat form layout
  mobile considerations
  add file
  initial layout
  Support local development of index.html
This commit is contained in:
Gabe Kangas 2020-06-13 22:46:06 -07:00
commit 6d8e8a8849
5 changed files with 570 additions and 107 deletions

View File

@ -6,7 +6,9 @@
href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
rel="stylesheet"
/>
<link href="./styles/layout.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- unpkg : use the latest version of Video.js -->
<link href="//unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<link
@ -14,104 +16,127 @@
rel="stylesheet"
/>
<script src="//unpkg.com/video.js/dist/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="//vjs.zencdn.net/7.8.2/video.min.js"></script>
<!-- Used for animating the scrolling of the chat div. Can that be done other ways? -->
<script src="vendor/jquery-2.1.4.min.js"></script>
<script src="vendor/autolink.js"></script>
</head>
<div>
<div class="flex">
<div class="w-4/6">
<video
id="video"
class="video-js vjs-theme-fantasy"
preload="auto"
poster="/thumbnail.png"
autoplay
controls
style="width: 100%; height: 600px;"
data-setup='{}'
>
<source src="hls/stream.m3u8" type="application/x-mpegURL"/>
</video>
<div id="app">
{{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}.
Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }},
{{ overallMaxViewerCount }} overall.
<body>
<div id="app-container" class="flex no-chat">
<header class="flex">
<h1>
😈 Owncast
</h1>
<div id="user-options-container" class="flex">
<div id="user-info">
<div id="user-info-display" title="Click to update user name" class="flex">
<img src="https://robohash.org/username123" id="username-avatar" class="rounded-full" />
<span id="username-display">Random Username 123</span>
</div>
<div id="user-info-change">
<input type="text"
id="username-change-input"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-1 px-1 leading-tight focus:bg-white"
value="Random Username 123"
maxlength="100"
placeholder="Update username"
>
<button id="button-update-username" class="bg-blue-500 hover:bg-blue-700 text-white py-1 px-1 rounded user-btn">Update</button>
<button id="button-cancel-change" class="bg-gray-900 hover:bg-gray-800 py-1 px-2 rounded user-btn" title="cancel">X</button>
</div>
</div>
<div id="chat-toggle" class="flex">💬</div>
</div>
</div>
<div class="w-2/6">
<div
id="messages-container"
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
style="height: 60vh; overflow-y: scroll;"
>
<div v-for="(message, index) in messages">
<div class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="flex items-center">
<img
v-bind:src="message.image"
class="w-10 h-10 rounded-full mr-4 border-black-500"
style="padding: 5px; background-color: #ececec;"
/>
</header>
<div id="main-content-container" class="flex">
<!-- LEFT CONTAINER SIDE-->
<div class="flex main-cols left-col">
<div class="text-sm">
<p class="text-700">{{ message.author }}</p>
<p class="text-gray-600"v-html="message.linkedText()"></p>
<div id="video-container" class="flex shadow-md">
<video
class="video-js vjs-theme-fantasy"
id="video"
preload="auto"
controls
autoplay
muted
poster="https://goth.land/thumbnail.png"
data-setup='{}'
>
<source src="https://goth.land/hls/stream.m3u8" type="application/x-mpegURL"/>
</video>
</div>
<div id="stream-info">
{{ streamStatus }} {{ viewerCount }} {{ 'viewer' | plural(viewerCount) }}.
Max {{ sessionMaxViewerCount }} {{ 'viewer' | plural(sessionMaxViewerCount) }},
{{ overallMaxViewerCount }} overall.
</div>
</div>
<!-- RIGHT CONTAINER SIDE-->
<div class="flex main-cols right-col">
<div id="chat-container">
<div id="messages-container">
<div v-for="(message, index) in messages">
<div class="message flex">
<img
v-bind:src="message.image"
class="message-avatar rounded-full"
/>
<div class="message-content">
<p class="message-author">{{ message.author }}</p>
<p class="message-text"v-html="message.formatText()"></p>
</div>
</div>
</div>
</div>
<div id="message-input-container" class="shadow-md">
<form id="message-form" class="flex" @submit="submitChatForm">
<input type="hidden" name="inputAuthor" id="self-message-author" v-model="message.author" />
<!-- Author -->
<!-- <label class="control-label" for="inputAuthor">Author</label>
<input
id="inputAuthor"
type="text"
class="appearance-none bg-gray-200 text-gray-700 border border-black-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
placeholder="Name"
v-model="message.author"
/> -->
<textarea
id="inputBody"
placeholder="Message"
v-model="message.body"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 leading-tight focus:bg-white"
></textarea>
<div id="message-form-actions" class="flex">
<span id="message-form-warning"></span>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded"
> Send
</button>
</div>
</form>
</div>
</div>
</div>
<form
id="chatForm"
@submit="submitChatForm"
class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
>
<!-- Author -->
<label class="control-label" for="inputAuthor">Author</label>
<input
id="inputAuthor"
type="text"
class="appearance-none bg-gray-200 text-gray-700 border border-black-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
placeholder="Name"
v-model="message.author"
/>
<!-- Body -->
<div>
<label class="control-label" for="inputBody">Message</label>
<div class="controls">
<textarea
id="inputBody"
placeholder="Message"
v-model="message.body"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
>
</textarea>
</div>
</div>
<div class="control-group">
<div class="controls">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Send
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="js/message.js"></script>
<script src="js/app.js"></script>
<script src="js/message.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View File

@ -1,19 +1,17 @@
function setupApp() {
Vue.filter('plural', function (string, count) {
if (count === 1) {
return string
return string;
} else {
return string + "s"
return string + "s";
}
})
window.app = new Vue({
el: "#app",
el: "#stream-info",
data: {
streamStatus: "",
viewerCount: 0,
sessionMaxViewerCount: 0,
overallMaxViewerCount: 0
},
});
@ -28,27 +26,30 @@ function setupApp() {
el: "#chatForm",
data: {
message: {
author: localStorage.author || "Viewer" + (Math.floor(Math.random() * 42) + 1),
author: "",//localStorage.author || "Viewer" + (Math.floor(Math.random() * 42) + 1),
body: ""
}
},
methods: {
submitChatForm: function (e) {
const message = new Message(this.message)
message.id = uuidv4()
localStorage.author = message.author
const messageJSON = JSON.stringify(message)
window.ws.send(messageJSON)
e.preventDefault()
const message = new Message(this.message);
message.id = uuidv4();
localStorage.author = message.author;
const messageJSON = JSON.stringify(message);
window.ws.send(messageJSON);
e.preventDefault();
this.message.body = ""
this.message.body = "";
}
}
});
var appMessagingMisc = new Messaging();
appMessagingMisc.init();
}
async function getStatus() {
const url = "/status";
let url = "https://util.real-ity.com:8042/status";
try {
const response = await fetch(url);
@ -60,7 +61,7 @@ async function getStatus() {
app.viewerCount = status.viewerCount
app.sessionMaxViewerCount = status.sessionMaxViewerCount
app.overallMaxViewerCount = status.overallMaxViewerCount
} catch (e) {
app.streamStatus = "Stream server is offline."
app.viewerCount = 0
@ -68,12 +69,12 @@ async function getStatus() {
}
var websocketReconnectTimer
var websocketReconnectTimer;
function setupWebsocket() {
clearTimeout(websocketReconnectTimer)
const protocol = location.protocol == "https:" ? "wss" : "ws"
var ws = new WebSocket(protocol + "://" + location.host + "/entry")
var ws = new WebSocket("wss://util.real-ity.com:8042/entry")
ws.onmessage = (e) => {
const model = JSON.parse(e.data)
@ -108,10 +109,10 @@ function setupWebsocket() {
setupApp()
getStatus()
setupWebsocket()
setInterval(getStatus, 5000)
// setInterval(getStatus, 5000)
function scrollSmoothToBottom(id) {
const div = document.getElementById(id)
const div = document.getElementById(id);
$('#' + id).animate({
scrollTop: div.scrollHeight - div.clientHeight
}, 500)

View File

@ -6,8 +6,13 @@ class Message {
this.id = model.id
}
linkedText() {
return autoLink(this.body, { embed: true })
addNewlines(str) {
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
}
formatText() {
var linked = autoLink(this.body, { embed: true });
return this.addNewlines(linked);
}
toModel() {
@ -18,4 +23,127 @@ class Message {
id: this.id
}
}
}
// convert newlines to <br>s
class Messaging {
constructor() {
this.chatDisplayed = false;
this.username = "";
this.avatarSource = "https://robohash.org/";
this.messageCharCount = 0;
this.maxMessageLength = 500;
this.maxMessageBuffer = 20;
this.tagChatToggle = document.querySelector("#chat-toggle");
this.tagUserInfoDisplay = document.querySelector("#user-info-display");
this.tagUserInfoChanger = document.querySelector("#user-info-change");
this.tagUsernameDisplay = document.querySelector("#username-display");
this.imgUsernameAvatar = document.querySelector("#username-avatar");
this.tagMessageAuthor = document.querySelector("#self-message-author");
this.tagMessageFormWarning = document.querySelector("#message-form-warning");
this.tagAppContainer = document.querySelector("#app-container");
this.inputChangeUserName = document.querySelector("#username-change-input");
this.btnUpdateUserName = document.querySelector("#button-update-username");
this.btnCancelUpdateUsername = document.querySelector("#button-cancel-change");
this.formMessageInput = document.querySelector("#inputBody");
}
init() {
this.tagChatToggle.addEventListener("click", this.handleChatToggle);
this.tagUsernameDisplay.addEventListener("click", this.handleShowChangeNameForm);
this.btnUpdateUserName.addEventListener("click", this.handleUpdateUsername);
this.btnCancelUpdateUsername.addEventListener("click", this.handleHideChangeNameForm);
this.inputChangeUserName.addEventListener("keydown", this.handleUsernameKeydown);
this.formMessageInput.addEventListener("keydown", this.handleMessageInputKeydown);
}
handleChatToggle = () => {
if (this.chatDisplayed) {
this.tagAppContainer.className = "flex no-chat";
this.chatDisplayed = false;
} else {
this.tagAppContainer.className = "flex";
this.chatDisplayed = true;
}
}
handleShowChangeNameForm = () => {
this.tagUserInfoDisplay.style.display = "none";
this.tagUserInfoChanger.style.display = "flex";
}
handleHideChangeNameForm = () => {
this.tagUserInfoDisplay.style.display = "flex";
this.tagUserInfoChanger.style.display = "none";
}
handleUpdateUsername = () => {
var newValue = this.inputChangeUserName.value;
newValue = newValue.trim();
// do other string cleanup?
if (newValue) {
this.userName = newValue;
this.inputChangeUserName.value = newValue;
this.tagMessageAuthor.innerText = newValue;
this.tagUsernameDisplay.innerText = newValue;
this.imgUsernameAvatar.src = this.avatarSource + newValue;
}
this.handleHideChangeNameForm();
}
handleUsernameKeydown = event => {
if (event.keyCode === 13) { // enter
this.handleUpdateUsername();
} else if (event.keyCode === 27) { // esc
this.handleHideChangeNameForm();
}
}
handleMessageInputKeydown = event => {
var okCodes = [37,38,39,40,16,91,18,46,8];
var value = this.formMessageInput.value.trim();
var numCharsLeft = this.maxMessageLength - value.length;
if (event.keyCode === 13) { // enter
if (!this.prepNewLine) {
// submit()
event.preventDefault();
// clear out things.
this.formMessageInput.value = "";
this.tagMessageFormWarning.innerText = "";
return;
}
this.prepNewLine = false;
} else {
this.prepNewLine = false;
}
if (event.keyCode === 16 || event.keyCode === 17) { // ctrl, shift
this.prepNewLine = true;
}
if (numCharsLeft <= this.maxMessageBuffer) {
this.tagMessageFormWarning.innerText = numCharsLeft + " chars left";
if (numCharsLeft <= 0 && !okCodes.includes(event.keyCode)) {
event.preventDefault();
return;
}
} else {
this.tagMessageFormWarning.innerText = "";
}
}
}

309
webroot/styles/layout.css Normal file
View File

@ -0,0 +1,309 @@
/* variables */
:root {
--header-height: 3em;
--right-col-width: 24em;
--chat-bg-color: rgba(11,0,33,.95);
--header-bg-color: rgba(20,0,40,1);
}
body {
font-size: 14px;
background-color: #666;
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
}
#app-container {
width: 100%;
flex-direction: column;
justify-content: flex-start;
position: relative;
color: white;
}
header {
position: fixed;
width: 100%;
height: var(--header-height);
top: 0;
left: 0;
background-color: var(--header-bg-color);
z-index: 10;
flex-direction: row;
justify-content: space-between;
}
header h1 {
font-size: 1.25em;
font-weight: 100;
letter-spacing: 1.2;
text-transform: uppercase;
color: #ddd;
padding: .5em;
white-space: nowrap;
}
#chat-toggle {
cursor: pointer;
background-color: #555;
text-align: center;
height: 100%;
width: 3em;
justify-content: center;
align-items: center;
}
#chat-toggle:hover {
background-color: #666;
}
/* ************************************************8 */
#user-options-container {
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
#user-info-display {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
cursor: pointer;
padding: .5em 1em;
}
#username-avatar {
height: 1.75em;
width: 1.75em;
margin-right: .5em;
border: 1px solid rgba(255,255,255,.25)
}
#username-display {
font-weight: bold;
font-size: .75em;
color: #516FEB
}
#user-info-display:hover {
transition: opacity .2s;
opacity: .75;
}
#user-info-change {
display: none;
justify-content: flex-end;
align-items: center;
padding: .25em;
}
#username-change-input {
font-size: .75em;
}
#button-update-username {
font-size: .65em;
text-transform: uppercase;
height: 2.5em;
}
#button-cancel-change {
color: rgba(255,255,255,.5);
cursor: pointer;
height: 2.5em;
font-size: .65em;
}
.user-btn {
margin: 0 .25em;
}
/* ************************************************8 */
#main-content-container {
width: 100%;
flex-direction: row;
position: relative;
margin-top: var(--header-height);
}
.main-cols {
flex-direction: column;
justify-content: flex-start;
position: relative;
}
.left-col {
width: calc(100vw - var(--right-col-width));
}
/* ************************************************8 */
#video-container {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
#video-container video {
width: 100%;
display: block;
}
#stream-info {
padding: .5em;
text-align: center;
font-family: monospace;
font-size: .75em;
background-color: rgba(0,0,0,.5);
border-bottom: 1px solid black;
}
/* ************************************************8 */
#chat-container {
position: fixed;
z-index: 9;
right: 0;
height: 100%;
width: var(--right-col-width);
background-color: var(--chat-bg-color);
height: calc(100vh - var(--header-height));
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
#messages-container {
overflow: auto;
padding: 1em 0;
}
#message-input-container {
width: 100%;
border-top: 1px solid #eee;
padding: 1em;
background-color: #334;
}
#message-form {
flex-direction: column;
align-items: flex-end;
margin-bottom: 0;
}
#message-form-actions {
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
}
#message-form-warning {
font-size: .75em;
color: red;
}
/* ************************************************8 */
.message {
padding: .85em;
align-items: flex-start;
}
.message-avatar {
height: 2.5em;
width: 2.5em;
margin-right: .75em;
background-color: rgba(0,0,0, .75);
}
.message-content {
font-size: .85em;
}
.message-content a {
color: #6699cc;
}
.message-content a:hover {
text-decoration: underline;
}
.message-author {
font-weight: 600;
}
.message-text {
color: #ccc;
font-weight: 100;
}
/* ************************************************8 */
.no-chat .left-col {
width: 100vw;
}
.no-chat .right-col {
display: none;
}
.no-chat #chat-toggle {
opacity: .5;
}
/* ************************************************8 */
@media screen and (max-width: 860px) {
:root {
--right-col-width: 20em;
}
#chat-container {
width: var(--right-col-width);
}
.left-col {
width: calc(100vw - var(--right-col-width));
}
}
@media screen and (max-width: 640px ) and (orientation: portrait) {
#main-content-container {
flex-direction: column;
justify-content: space-between;
height: calc(100vh - var(--header-height));
}
.main-cols {
width: 100vw;
}
.left-col {
flex-direction: column;
justify-content: stretch;
}
.right-col {
overflow: hidden;
}
#info {
display: none;
overflow: auto;
height: auto;
}
#chat-container {
width: 100%;
height: 100%;
position: relative;
height: auto;
}
.no-chat .left-col {
height: 100%;
}
.no-chat .right-col {
display: none;
}
.no-chat #info {
display: block;
}
}

View File

@ -113,7 +113,7 @@ AutoLink.prototype = {
var text = this.options.removeHTTP ? removeHTTP(match) : match
return (
p1 +
'<a href="' +
'<a target="_blank" href="' +
match +
'"' +
this.attrs +