This is the template project that's checked out and configured when you run the bando-up command from ljsthw-bandolier. This is where the code really lives.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
bandolier-template/client/pages/Live.svelte

287 lines
9.1 KiB

<script>
import DiscordReplay from "$/client/components/DiscordReplay.svelte";
import HLSVideo from "$/client/components/HLSVideo.svelte";
import WTVideo from "$/client/components/WTVideo.svelte";
import Icon from "$/client/components/Icon.svelte";
import IconImage from "$/client/components/IconImage.svelte";
import Layout from '$/client/Layout.svelte';
import Markdown from "$/client/components/Markdown.svelte";
import parseISO from "date-fns/parseISO";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import { defer } from "$/client/helpers.js";
import { onMount } from 'svelte';
import Toasts from "$/client/components/Toasts.svelte";
import SnapImage from "$/client/components/SnapImage.svelte";
import { link } from "svelte-spa-router";
import { load_past_streams, load_stream, change_stream } from "$/client/livestreams.js";
import { user } from "$/client/stores.js";
import { connect_socket, configure_socket } from "$/client/websocket.js";
export let params = {};
let livestream_id = params.livestream_id;
const autoplay = true;
let socket;
let send_toast;
let stream = {};
let load_defer = defer();
let past_defer = defer();
const show_free_archived = (stream) => {
return stream.state === "archived" && ($user.authenticated || stream.is_free);
}
const load_all = async () => {
stream = await load_stream(livestream_id, false);
load_defer.resolve(stream);
if(stream.state === "live") {
send_toast("Hit play if the video doesn't start.");
}
const past_streams = await load_past_streams();
past_defer.resolve(past_streams);
}
const change_video_state = async () => {
const status = await change_stream(livestream_id);
if(status === 200) {
stream = await load_stream(livestream_id, false);
if(stream.state === "live") {
send_toast("Stream is live!");
}
} else {
send_toast("Request failed.");
}
}
$: if(livestream_id !== params.livestream_id) {
livestream_id = params.livestream_id;
load_defer = defer();
past_defer = defer();
load_all();
}
onMount(async () => {
await configure_socket();
socket = connect_socket();
socket.on("/live/update", async (data) => {
if(data.message) send_toast(data.message);
load_all();
});
await load_all();
});
</script>
<style>
container {
display: flex;
flex-direction: row;
width: 100%;
}
container > left {
width: 100%;
max-height: calc(100vh - var(--fixed-header-height));
overflow-y: auto;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
aspect-ratio: 16/9;
}
/* This combined with the -ms-overflow-style and scrollbar-width is how you remove
the scrolls on the inner video/notes column.
*/
container > left::-webkit-scrollbar {
display: none;
}
container > left notes {
display: flex;
flex-direction: column;
padding-left: 1rem;
padding-right: 1rem;
}
container > waiting {
display:flex;
width: 100%;
min-height: calc(100vh - var(--fixed-header-height));
justify-content: center;
align-items: center;
}
container > right {
min-width: 400px;
max-width: 400px;
height: calc(100vh - var(--fixed-header-height));
border-left: 1px solid var(--value7);
}
container > right.video-list {
padding-left: 0.5rem;
min-height: calc(100vh - var(--fixed-header-height));
max-height: calc(100vh - var(--fixed-header-height));
overflow-y: auto;
}
container > right card {
border: unset;
box-shadow: unset;
}
container > right card middle {
margin-bottom: 0.3rem;
}
span#view-count {
display: flex;
align-content: center;
}
@media only screen and (max-width: 800px) {
right {
display: none;
}
}
</style>
<Layout centered={ true } fullwidth={ true } fixed={ true } footer={ false } auth_optional={ true } testid="live-page">
<container>
{#await load_defer}
<br/>
{:then}
<left>
{#if stream.state === "live"}
<HLSVideo autoplay={ autoplay } poster={ stream.poster } source={ stream.source } />
{:else if stream.state === "archived" }
{#if show_free_archived(stream)}
<WTVideo media={ stream.media } />
{:else if stream.poster }
<SnapImage src={ stream.poster } />
{:else}
<IconImage height="unset" aspect_ratio="16/9" name="film" />
{/if}
{:else if stream.state === "finished"}
<IconImage height="unset" aspect_ratio="16/9" name="video-off" />
{:else if stream.state === "pending"}
<IconImage height="unset" aspect_ratio="16/9" name="clock" />
{:else}
<IconImage height="unset" aspect_ratio="16/9" name="video" />
{/if}
<info>
<tile>
<left>
<Icon name="user" size="48" />
</left>
<middle>
<b>Zed A. Shaw</b>
<span>{ stream.title }</span>
</middle>
<right>
{#if $user.admin}
{#if stream.state === "live"}
<span on:click={ () => change_video_state() }>
<Icon name="stop-circle" tip_position="bottom-left" tooltip="Stop stream." size="48" color="var(--red)"/>
</span>
{:else if stream.state === "pending"}
<span on:click={ () => change_video_state() }>
<Icon name="arrow-up-circle" tip_position="bottom-left" tooltip="Announce ready." size="48" color="var(--yellow)"/>
</span>
{:else if stream.state === "ready"}
<span on:click={ () => change_video_state() }>
<Icon name="cast" size="48" tip_position="bottom-left" tooltip="Go live." color="var(--green)"/>
</span>
{:else}
<a use:link href="/admin/table/livestream/{ stream.id }/">Finished</a>
{/if}
{:else}
<!--
<span id="view-count">
<Icon name="user" size="24" />&nbsp;{ stream.viewer_count }
</span>
-->
{/if}
</right>
</tile>
</info>
<notes>
<Markdown content={ stream.description } />
<br>
{#if stream.state === "pending"}
<callout class="info">
<span data-testid="pending-message">
This live stream starts at <b>{ stream.starts_on }</b>.
Come back then, and hit refresh.
</span>
</callout>
{:else if stream.state === "ready"}
<callout class="info">
<span>
The show is about to start. Your page might refresh when it does, but if the video doesn't start on its own hit refresh.
</span>
</callout>
{:else if stream.state === "finished"}
<callout class="info">
<span>
This live stream has ended. It will be posted for later viewing after editing, probably in a few days.
</span>
</callout>
{:else if stream.state === "archived"}
<callout class="info">
<span>
{#if $user.authenticated}
I hope you enjoyed the stream. If you find a mistake or a problem with the video then please tell me in the chat.
{:else if show_free_archived(stream) }
If you enjoyed this free livestream and would like to learn more JavaScript, then <a style="color: var(--value0)" href="/client/#/register/">register today</a> to get even more livestreams and an <b>entire course in JavaScript</b>.
{:else}
This live stream is for members only. If you want to learn JavaScript, then <a style="color: var(--value0)" href="/client/#/register/">register today</a> and enjoy all livestreams plus all modules.
{/if}
</span>
</callout>
{/if}
</notes>
</left>
{#if ["pending", "finished", "archived"].includes(stream.state)}
<right class="video-list">
{#await past_defer then streams}
{#each streams as archived}
{#if archived.id !== stream.id}
<card>
<top>
<a data-testid="past-link-{ archived.id }" use:link href="/live/{ archived.id }/">
{#if archived.poster}
<SnapImage src={ archived.poster } />
{:else}
<IconImage name="video" />
{/if}
</a>
</top>
<middle>
<b>{ archived.title }</b>
<div>{ formatDistanceToNow(parseISO(archived.starts_on), {addSuffix: true}) }</div>
</middle>
</card>
{/if}
{/each}
{/await}
</right>
{:else}
<right>
<DiscordReplay />
</right>
{/if}
{/await}
</container>
<Toasts bind:send_toast fade_after={ 10000 } />
</Layout>