Browse Source

First rewriting of the zedshaw.games code using the latest.

master
Zed A. Shaw 2 months ago
parent
commit
b4dfeefe59
  1. 27
      api/media_like.js
  2. 9
      client/components/IsVisible.svelte
  3. 145
      client/components/VideoDisplay.svelte
  4. 2
      client/helpers.js
  5. 145
      client/pages/Home.svelte
  6. 113
      client/pages/Video.svelte
  7. 202
      client/pages/VideoIndex.svelte
  8. 2
      client/routes.js
  9. 2
      commands/convert.js
  10. 75
      commands/lorem.js
  11. 14
      commands/media.js
  12. 13
      migrations/20220603163208_likes_on_videos.cjs
  13. 2
      package.json
  14. 4
      rendered/Header.svelte
  15. 107
      rendered/pages/video/[slug]/index.svelte
  16. 2
      static/color.css
  17. 11
      static/global.css

27
api/media_like.js

@ -0,0 +1,27 @@
import { Media } from "../lib/models.js";
import { knex } from "../lib/ormish.js";
import logging from '../lib/logging.js';
import assert from 'assert';
import { API } from '../lib/api.js';
const log = logging.create(import.meta.url);
export const get = async (req, res) => {
const api = new API(req, res);
try {
const { media_id, really } = req.query;
assert(media_id !== undefined, "media_id required");
assert(really !== undefined, "really required");
if(really) {
await knex(Media.table_name).increment('likes').where({id: media_id});
} else {
await knex(Media.table_name).increment('dislikes').where({id: media_id});
}
api.reply(200, {message: "OK" });
} catch (error) {
log.error(error);
api.error(500, error.message || "Internal Server Error");
}
}

9
client/components/IsVisible.svelte

@ -3,19 +3,22 @@
import { createEventDispatcher } from "svelte";
export let options = {};
// set this to never show the visible slot
export let debug = false;
export let visible = false;
export let idempotent = false; // this will make the visible slot stick
// this will make the visible slot stick
export let idempotent = false;
const dispatch = createEventDispatcher();
const show = (entry) => {
visible = true;
visible = !debug;
dispatch("visible", entry);
}
const hide = (entry) => {
// only do the hide if status is false
if(!idempotent) {
visible = false;
visible = debug;
dispatch("hidden", entry);
}
}

145
client/components/VideoDisplay.svelte

@ -0,0 +1,145 @@
<script>
import { createEventDispatcher } from "svelte";
import { link } from 'svelte-spa-router';
import api from "$/client/api.js";
import Icon from "$/client/components/Icon.svelte";
import WTVideo from "$/client/components/WTVideo.svelte";
import Video from "$/client/components/Video.svelte";
import Toasts from "$/client/components/Toasts.svelte";
import { base_host } from "$/client/config.js";
import { user } from "$/client/stores.js";
export let media;
export let embed = false;
let dispatch = createEventDispatcher();
let send_toast;
const like = async (video, really) => {
await api.get("/api/media_like", {
media_id: video.id, really
});
if(really) {
video.likes += 1;
} else {
video.dislikes += 1;
}
media = video;
}
const copy_share = async (media) => {
await navigator.clipboard.writeText(`${base_host}/video/${ media.id}-${ media.slug }/`);
send_toast("Link copied.");
}
</script>
<style>
content {
flex-direction: row-reverse;
background-color: var(--value2);
}
content > left {
min-height: calc(100vh);
max-height: calc(100vh);
padding: 0.5rem;
width: 400px;
min-width: 400px;
max-width: 400px;
color: var(--value9);
background-color: var(--value0);
}
content > right {
background-color: var(--value1);
width: 100%;
}
content > right close {
position: absolute;
top: 5px;
left: 5px;
}
video-display {
display: flex;
background-color: var(--value1);
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 2rem;
padding-bottom: 1rem;
}
info > stats {
display: flex;
justify-content: space-around;
border-bottom: 1px solid var(--value5);
}
info > likes {
display: flex;
justify-content: flex-end;
color: var(--value3);
}
info > h4 {
font-size: 2rem;
}
</style>
<content>
<left>
<info>
<stats>
<span>
<Icon name="user" color="var(--value9)" /> {media.views}
</span>
{#if $user.authenticated && $user.admin}
<a href="/admin/table/media/{ media.id }/" use:link><Icon name="database" color="var(--value9)" /></a>
{/if}
<span on:click={ () => copy_share(media) }>
<Icon name="share" color="var(--value9)" />
</span>
</stats>
<h4>{media.title}</h4>
<p>{media.description}</p>
<likes class="no-select">
<span on:click={ () => like(media, true) }>
<Icon name="thumbs-up" color="var(--value3)" />{ media.likes }
</span>
&nbsp;
<span on:click={ () => like(media, false) }>
<Icon name="thumbs-down" color="var(--value3)" />{ media.dislikes }
<span>
</likes>
</info>
</left>
<right>
{#if embed}
<close on:click={ () => dispatch("close") }>
<Icon name="x-circle" color="var(--value3)" />
</close>
{:else}
<close>
<a href="/client/">
<Icon name="home" color="var(--value3)" />
</a>
</close>
{/if}
<video-display>
{#if media.torrent_url}
<WTVideo media={ media } />
{:else}
<Video src={ media.src } poster={ media.poster } />
{/if}
</video-display>
</right>
</content>
<Toasts bind:send_toast orientation={ "bottom right" } />

2
client/helpers.js

@ -148,5 +148,5 @@ export const resize = (node, options) => {
export const page_change = (obj, params, key="id") => {
// using != on purpose for the conversion
return obj[key] !== undefined && obj[key] != params[key];
return obj !== undefined && obj[key] !== undefined && obj[key] != params[key];
}

145
client/pages/Home.svelte

@ -1,36 +1,123 @@
<script>
import Admin from "$/client/pages/admin/index.svelte";
import LoggedIn from "$/client/components/LoggedIn.svelte";
import Header from "$/client/Header.svelte";
import Layout from "$/client/Layout.svelte";
import { onMount } from "svelte";
import api from "$/client/api.js";
import {link} from 'svelte-spa-router';
import Icon from "$/client/components/Icon.svelte";
import IconImage from "$/client/components/IconImage.svelte";
import SnapImage from "$/client/components/SnapImage.svelte";
let activate_warning = false;
let auth_required = false;
let available_media = [];
onMount(() => {
setTimeout(() => activate_warning = true, 1000);
onMount(async () => {
const [status, data] = await api.get("/api/media");
if(status === 200) {
available_media = data;
} else {
console.error("Invalid response", status, data);
}
});
</script>
<LoggedIn redirect="/login/" show_required_page={ true }>
<div slot="yes" data-testid="home-page">
{#if auth_required}
<Header fixed={false}/>
<main>
<h1>Admin Required</h1>
<p>Your account isn't an administrator yet. Finish the <code>bando init</code> instructions and click the button:</p>
<button type="button" on:click={ () => auth_required = false }>Reload Database Admin</button>
</main>
{:else}
<Admin on:auth_required={ () => auth_required = true } />
{/if}
<toast-list class="bottom right active" class:active={ activate_warning }>
<toast>
Edit <code>client/pages/Home.svelte</code> to start your
project. Use ctrl-alt-b to view the tools.
</toast>
</toast-list>
</div>
</LoggedIn>
<style>
videos {
display: flex;
flex-direction: column;
width: 100%;
align-self: start;
}
content > left {
padding: 0.5rem;
}
a#login button {
margin-top: 1rem;
width: 100%;
background-color: var(--value9);
color: var(--value3);
border-color: var(--value3);
}
a#login button:hover {
filter: unset;
background-color: var(--value9);
color: var(--value0);
border-color: var(--value0);
}
a#login:hover {
filter: unset;
}
card {
margin-bottom: 1rem;
border: 0px;
border-bottom: 1px solid var(--value7);
border-radius: 0px;
box-shadow: unset;
}
card middle {
padding-left: 0px;
padding-right: 0px;
}
video-stats {
display: flex;
justify-content: space-between;
color: var(--value3);
}
</style>
<Layout fixed={ false } authenticated={ true } fullscreen={ true } footer={ false } testid="home-page">
<content>
<left>
<div>Log in to watch the live streams and comment on Zed's terrible gaming. It's free.</div>
<a id="login" href="/client/#/login/">
<button id="login" type="button">Log In</button>
</a>
<hr>
<div>Discover</div>
<tags>#tags #here</tags>
<hr>
<div>
Follow me on <a href="https://twitter.com/lzsthw" target="_blank"><Icon name="twitter" /></a> <a href="https://instagram.com/zedshaw" target="_blank"><Icon name="instagram" /></a> or <a href="mailto:zed@zedshaw.games">email me</a>.
</div>
</left>
<right>
<videos>
{#each available_media as media}
<card>
<middle>
<a href="/video/{ media.id }/" use:link>
{#if media.poster}
<SnapImage width={ media.width } height={ media.height } src={ media.poster } />
{:else}
<IconImage name="video" />
{/if}
</a>
</middle>
<bottom>
<h4>{ media.title }</h4>
<video-stats>
<span><Icon name="user" color="var(--value3)" /> { media.views }</span>
<span><Icon name="thumbs-up" color="var(--value3)" /> { media.likes }</span>
<span><Icon name="thumbs-down" color="var(--value3)" /> { media.dislikes }</span>
<span>{ media.tags }</span>
</video-stats>
</bottom>
</card>
{/each}
</videos>
</right>
</Layout>

113
client/pages/Video.svelte

@ -1,24 +1,14 @@
<script>
import Layout from "$/client/Layout.svelte";
import { onMount } from "svelte";
import { onMount } from "svelte";
import api from "$/client/api.js";
import {link} from 'svelte-spa-router';
import { defer, page_change } from "$/client/helpers.js";
import Icon from "$/client/components/Icon.svelte";
import WTVideo from "$/client/components/WTVideo.svelte";
import Video from "$/client/components/Video.svelte";
import IconImage from "$/client/components/IconImage.svelte";
import SnapImage from "$/client/components/SnapImage.svelte";
import VideoDisplay from "$/client/components/VideoDisplay.svelte";
export let params = {};
let media = {};
let media;
let media_defer = defer();
const load_related = async () => {
const [ status, data ] = await api.get("/api/media");
return status == 200 ? data : [];
}
const load_media = async () => {
const [status, data] = await api.get("/api/media", {
media_id: params.id
@ -38,83 +28,64 @@
load_media();
}
onMount(load_media);
onMount(async () => {
await load_media();
});
</script>
<style>
content {
padding-top: var(--fixed-header-height);
flex-direction: row-reverse;
background-color: var(--value2);
}
content > left {
overflow-y: auto;
min-height: calc(100vh - var(--fixed-header-height));
max-height: calc(100vh - var(--fixed-header-height));
min-height: calc(100vh);
max-height: calc(100vh);
padding: 0.5rem;
width: 400px;
min-width: 400px;
max-width: 400px;
color: var(--value9);
background-color: var(--value0);
}
content > right {
border: 1px solid green;
background-color: var(--value1);
width: 100%;
}
content > right close {
position: absolute;
top: 5px;
left: 5px;
}
video-display {
display: flex;
background-color: var(--value2);
background-color: var(--value1);
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 2rem;
padding-bottom: 1rem;
}
</style>
<Layout footer={ false } fixed={ true } authenticated={ true } fullwidth={ true }>
<content>
<left>
{#await load_related()}
&nbsp;
{:then related}
{#each related as media}
<card>
<top>
<a href="/video/{ media.id }/" use:link>
{#if media.poster}
<SnapImage width={ media.width / 3 } height={ media.height / 3 } src={ media.poster } />
{:else}
<IconImage name="video" />
{/if}
</a>
</top>
<bottom>{ media.id}</bottom>
</card>
{/each}
{/await}
</left>
info stats {
display: flex;
justify-content: space-around;
border-bottom: 1px solid var(--value5);
}
<right>
{#await media_defer}
&nbsp;
{:then}
<video-display style="--aspect-ratio: 16/9;">
{#if media.torrent_url}
<WTVideo media={ media } />
{:else}
<Video src={ media.src } poster={ media.poster } />
{/if}
</video-display>
info h4 {
font-size: 2rem;
}
</style>
<info>
<tile>
<left>
<Icon name="user" size="48" />
</left>
<middle>
ID: { media.id } { media.title || "No title." } { media.duration }
</middle>
<right>
{ media.views } <Icon name="eye" />
</right>
</tile>
</info>
{/await}
</right>
</content>
<Layout header={ false } footer={ false } fixed={ false } authenticated={ true } fullwidth={ true }>
{#await media_defer}
&nbsp;
{:then}
<VideoDisplay media={ media } embed={ false } />
{/await}
</Layout>

202
client/pages/VideoIndex.svelte

@ -2,60 +2,200 @@
import Layout from "$/client/Layout.svelte";
import { onMount } from "svelte";
import api from "$/client/api.js";
import {link} from 'svelte-spa-router';
import { link } from 'svelte-spa-router';
import { user } from "$/client/stores.js";
import Icon from "$/client/components/Icon.svelte";
import IconImage from "$/client/components/IconImage.svelte";
import SnapImage from "$/client/components/SnapImage.svelte";
import IsVisible from "$/client/components/IsVisible.svelte";
import VideoDisplay from "$/client/components/VideoDisplay.svelte";
let available_media = [];
let displayed = [];
let paging = 5;
let page_count = paging;
let viewed_video;
const load_media = (media) => {
// if it's the last one load more
const last = displayed[displayed.length - 1];
if(media.id === last.id) {
page_count += paging;
displayed = available_media.slice(0, page_count);
}
}
const show_video = (media) => {
viewed_video = media;
}
const hide_video = () => {
viewed_video = undefined;
}
const handle_keypress = (event) => {
if(event.key === "Escape") {
viewed_video = undefined;
}
}
onMount(async () => {
const [status, data] = await api.get("/api/media");
if(status === 200) {
available_media = data;
displayed = data.slice(0, page_count);
} else {
console.error("Invalid response", status, data);
}
});
</script>
<svelte:window on:keydown={ handle_keypress } />
<style>
videos {
display: grid;
width: 100%;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto;
grid-gap: 0.5rem;
flex-direction: column;
align-self: start;
width: 100%;
}
content {
}
content > left {
padding: 0.5rem;
}
content > right {
padding-left: 1rem;
overflow-y: auto;
padding-top: 1rem;
width: 100%;
}
a#login {
margin-top: 1rem;
background-color: var(--value9);
color: var(--value3);
border-color: var(--value3);
}
a#login i {
}
a#login:hover {
filter: unset;
background-color: var(--value9);
color: var(--value0);
border-color: var(--value0);
}
card {
margin-bottom: 1rem;
border: 0px;
border-bottom: 1px solid var(--value7);
border-radius: 0px;
box-shadow: unset;
}
card middle {
padding-left: 0px;
padding-right: 0px;
}
video-stats {
display: flex;
justify-content: space-between;
color: var(--value3);
}
video-overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
}
content > left sidebar {
position: fixed;
height: 100%;
box-shadow: unset;
}
</style>
<Layout fixed={ false } authenticated={ true } fullscreen={ true } footer={ false } testid="home-page">
<videos>
{#each available_media as media}
<card>
<top>
<a href="/video/{ media.id }/" use:link>
{#if media.poster}
<SnapImage width={ media.width / 3 } height={ media.height / 3 } src={ media.poster } />
{:else}
<IconImage name="video" />
{/if}
</a>
</top>
<bottom>
<video-views>
<Icon name="user" /> { media.views }
</video-views>
<video-tags>
{ media.tags }
</video-tags>
</bottom>
</card>
{/each}
</videos>
<Layout fixed={ true } authenticated={ true } footer={ false } testid="home-page">
<content>
<left>
<sidebar>
<div>Log in to watch the live streams and comment on Zed's terrible gaming. It's free.</div>
<hr>
{#if $user.authenticated}
<div>Welcome Back <em>{ $user.full_name }!<em></div>
{:else}
<a id="login" href="/client/#/login/"><i>Log In</i></a>
{/if}
<hr>
<div>Discover</div>
<tags>#tags #here</tags>
<hr>
<div>
Follow me on <a href="https://twitter.com/lzsthw" target="_blank"><Icon name="twitter" /></a> <a href="https://instagram.com/zedshaw" target="_blank"><Icon name="instagram" /></a> or <a href="mailto:zed@zedshaw.games">email me</a>.
</div>
</sidebar>
</left>
<right>
<videos>
{#each displayed as media, i (media.id)}
<card>
<middle>
<IsVisible
options={ { threshold: 0 } }
on:visible={ () => load_media(media) }>
<div slot="visible">
<a href="/video/{ media.id }/" on:click|preventDefault={ show_video(media) }>
{#if media.poster}
<SnapImage width={ media.width } height={ media.height } src={ media.poster } />
{:else}
<IconImage name="video" width={ media.width } height={ media.height }/>
{/if}
</a>
</div>
<div slot="hidden" style="--aspect-ratio: 16/9;">
<p>&nbsp;</p>
</div>
</IsVisible>
</middle>
<bottom>
<h4>{ media.title }</h4>
<video-stats>
<span><Icon name="user" color="var(--value3)" /> { media.views }</span>
<span>{ media.tags }</span>
</video-stats>
</bottom>
</card>
{/each}
</videos>
</right>
</Layout>
{#if viewed_video}
<video-overlay>
<VideoDisplay
embed={ true }
media={ viewed_video }
on:close={ hide_video }
/>
</video-overlay>
{/if}

2
client/routes.js

@ -46,6 +46,6 @@ export default {
'/admin/email/': EmailConfig,
'/bando/components/:name?': Components,
// #endif */
'/': Home,
'/': VideoIndex,
'*': NotFound,
}

2
commands/convert.js

@ -20,7 +20,7 @@ export const options = [
["--speed <str>", "ffmpeg speed preset", "veryslow"],
["--crf <int>", "constant rate factor", 30],
["--debug <level>", "1=print the ffmpeg command, 2=and its stderr output"],
["--clean-filename", "don't modify the output file with scale info", true],
["--clean-filename", "don't modify the output file with scale info", false],
["--progress", "Show percent progress. Not accurate (thanks ffmpeg)", false],
["--output <string>", "specific output file name"],
["--outdir <string>", "write the file to this dir (can't combine with output)"],

75
commands/lorem.js

@ -0,0 +1,75 @@
import { Media } from "../lib/models.js";
import { knex } from "../lib/ormish.js";
import slugify from "slugify";
export const description = "Adds lorem ipsum text.";
const delorean = [
"Uh, plutonium, wait a minute, are you telling me that this sucker's nuclear?",
"Oh, no no no, I never uh, I never let anybody read my stories.",
"George, buddy.",
"Remember that girl I introduced you to, Lorraine.",
"What are you writing?",
"Indeed I will, roll em.",
"I, Doctor Emmett Brown, am about to embark on an historic journey.",
"What have I been thinking of, I almost forgot to bring some extra plutonium.",
"How did I ever expect to get back, one pallet one trip I must be out of my mind.",
"What is it Einy?",
"Oh my god, they found me, I don't know how but they found me.",
"Run for it, Marty.",
"Don't worry.",
"Yeah. George. George.",
"Hey, George, buddy, you weren't at school, what have you been doing all day?",
"I haven't Uh, coast guard.",
"I think I know exactly what you mean.",
"Now that's a risk you'll have to take you're life depends on it.",
"Perfect, just perfect.",
"Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.",
"I could hang out, you could show me around.",
"Wow, ah Red, you look great.",
"Everything looks great.",
"I still got time.",
"Oh my god.",
"No, no not again, c'mon, c'mon.",
];
const hipsum = "I'm baby taiyaki coloring book tacos mixtape church-key four dollar toast you probably haven't heard of them twee polaroid portland ennui +1 Artisan master cleanse shaman forage Shoreditch tacos pabst celiac hella cardigan tumeric truffaut neutra venmo beard you probably haven't heard of them 3 wolf moon vice Plaid cray taxidermy migas actually church-key street art PBR&B irony Glossier hot chicken pok pok +1 asymmetrical ennui snackwave unicorn messenger bag Pok pok farm-to-table humblebrag seitan artisan portland XOXO Church-key asymmetrical +1 palo santo tumblr trust fund flannel beard neutra skateboard banjo tonx Paleo iPhone hella fashion axe seitan hoodie unicorn knausgaard cronut post-ironic pug Banh mi keytar coloring book hot chicken tumeric health goth bespoke prism XOXO brooklyn gastropub enamel pin umami Slow-carb DIY lumbersexual sriracha vaporware direct trade fashion axe keffiyeh drinking vinegar cold-pressed next level Portland tattooed taiyaki tbh Cardigan normcore chicharrones man braid vape Pabst try-hard tofu craft beer bespoke asymmetrical Viral celiac mumblecore echo park jean shorts semiotics pitchfork lyft Cray sustainable scenester helvetica kombucha tumeric la croix blog waistcoat ethical art party squid slow-carb before they sold out Echo park meggings slow-carb stumptown cold-pressed godard fingerstache swag Succulents forage yuccie blog stumptown tote bag migas La croix readymade retro deep v taxidermy Cloud bread jianbing farm-to-table sriracha chartreuse pop-up taxidermy mixtape poutine Sartorial you probably haven't heard of them man braid tumblr Pop-up ugh hella hashtag thundercats art party artisan VHS skateboard yes plz church-key quinoa keytar VHS wolf small batch four dollar toast aesthetic mumblecore kombucha seitan poutine meditation heirloom Enamel pin plaid kinfolk subway tile Everyday carry street art organic direct trade VHS tattooed poke lumbersexual Kinfolk vinyl mixtape direct trade 90's kickstarter vexillologist jean shorts pop-up Dummy text More like dummy thicc text amirite".split(' ');
const random_element = (count, from) => {
const result = [];
for(let i = 0; i < count; i++) {
result.push(from[Math.floor(Math.random() * from.length)]);
}
return result;
}
const capitalize = (title) => {
return `${title[0].toUpperCase()}${title.slice(1)}`;
}
export const main = async () => {
const media = await Media.all({}, ["id"]);
for(let m of media) {
const title_length = Math.floor(Math.random() * 6) + 1;
const title_words = random_element(title_length, hipsum);
const title = title_words.map(t => capitalize(t)).join(' ');
const tag_words = random_element(title_length / 2, hipsum);
const tags = tag_words.map(t => "#" + slugify(t)).join(' ');
const desc = random_element(2, delorean).join(' ');
await Media.update({id: m.id}, {
title,
tags,
slug: slugify(title, { strict: true, lower: true, trim: true}),
description: desc,
views: Math.floor(Math.random() * 100)
});
}
knex.destroy();
}

14
commands/media.js

@ -110,14 +110,14 @@ export const video_meta = async (meta, opts={}) => {
return meta;
}
export const upsert_media = async(video, description) => {
export const upsert_media = async(video, desc) => {
const video_url = video.format.filename.slice(1)
const stream = video.streams[0];
const { name } = path.parse(video_url);
const data = {
"title": video.title,
"description": description,
"description": desc,
"created_at": video.format.created_at, // recorded at comes from the filename
"updated_at": video.format.ctime, // ctime works better as the update time
"src": video_url,
@ -129,7 +129,7 @@ export const upsert_media = async(video, description) => {
"preload": "auto",
"torrent_url": video.format.torrent,
"poster": video.poster ? video.poster.slice(1) : null,
"slug": slugify(name, { lower: true, strict: true, trim: true})
"slug": slugify(video.title || name, { lower: true, strict: true, trim: true})
}
// the db doesn't return the id unless it's an insert, which is stupid
@ -164,8 +164,12 @@ const load_media = async (opts) => {
opts.screenshot_at = parse_seek_times(opts.screenshot_at);
for(let fname of media_files) {
// strip off the leading . for video_meta
const video = await video_meta({src: fname.slice(1)}, opts);
// remove the leading . for the src URL
const src = fname.slice(1);
// attempt to find this media in the database
const original = await Media.first({src}) || { src };
const video = await video_meta(original, opts);
// use the episode number since ID could not represent the sequence
const media = await upsert_media(video, undefined);

13
migrations/20220603163208_likes_on_videos.cjs

@ -0,0 +1,13 @@
exports.up = async (knex) => {
await knex.schema.alterTable('media', (table) => {
table.integer('likes').default(0).notNullable();
table.integer('dislikes').default(0).notNullable();
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('media', (table) => {
table.dropColumn('likes');
table.dropColumn('dislikes');
});
};

2
package.json

@ -19,7 +19,7 @@
"test": "npx ava tests/**/*.js",
"modules": "./bando.js load",
"modules-watch": "nodemon --watch ../ljsthw-private/db/modules/ --ext .md,.js,.sh ./bando.js load",
"rendered-watch": "nodemon --ignore \"rendered/build/**/*\" --watch ./rendered --watch ../static -e md,svelte,js,css ./bando.js rendered",
"rendered-watch": "nodemon --ignore \"rendered/build/**/*\" --watch ./rendered --watch static -e md,svelte,js,css ./bando.js rendered",
"rendered": "./bando.js rendered",
"knex": "knex --knexfile=knexfile.cjs",
"coverage:test": "cross-env NODE_V8_COVERAGE=.coverage DANGER_ADMIN=1 DEBUG=1 npm run test",

4
rendered/Header.svelte

@ -9,8 +9,8 @@
<nav>
<a href="/"><Icon name="home" size="48" /></a>
<ul>
<li><a href="/video/"><Icon name="video" /></a></li>
<li><a href="/live/"><Icon name="cast" /></a></li>
<li><a href="/client/#/video/"><Icon name="video" /></a></li>
<li><a href="/client/#/live/"><Icon name="cast" /></a></li>
{#if register_enabled}
<li><a href="/client/#/register/"><Icon name="dollar-sign" /></a></li>

107
rendered/pages/video/[slug]/index.svelte

@ -0,0 +1,107 @@
<script context="module">
import { get } from "$/rendered/pages/live/index.js";
import { Hydrate } from '@jamcart/7ty/components';
import { twitter_user } from "$/client/config.js";
export const getPaths = async () => {
let data = await get("/api/media");
return data.map(m => {
m.slug = `${m.id}-${m.slug}`;
return m;
});
}
export const getData = media => {
return {slug: media.id, media }
}
</script>
<script>
import Layout from "$/rendered/Layout.svelte";
import VideoDisplay from "$/client/components/VideoDisplay.svelte";
import OGPreview from "$/client/components/OGPreview.svelte";
export let media;
export let og = {
"title": media.title,
"description": media.description,
"image": media.poster,
"url": media.slug, // URL to article
"type": "website", // not mentioned on linked in but needed
}
// taken from https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup
export let twitter = {
"card": "summary", // must be summary
"creator": twitter_user, // @username of content creator
"description": og.description, // max 200 chars
"image": og.image, // max 5mb image url 1:1 ratio
"image:alt": og.title, // max 420 chars image alt
"site": twitter_user, // @username of site
"title": og.title, // max 70 chars title
"player:width": media.width, // width iframe in pixels
"player:height": media.height, // height iframe in pixels
}
</script>
<style>
content {
flex-direction: row-reverse;
background-color: var(--value2);
}
content > left {
min-height: calc(100vh);
max-height: calc(100vh);
padding: 0.5rem;
width: 400px;
min-width: 400px;
max-width: 400px;
color: var(--value9);
background-color: var(--value0);
}
content > right {
background-color: var(--value1);
width: 100%;
}
content > right close {
position: absolute;
top: 5px;
left: 5px;
}
video-display {
display: flex;
background-color: var(--value1);
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 2rem;
padding-bottom: 1rem;
}
info stats {
display: flex;
justify-content: space-around;
border-bottom: 1px solid var(--value5);
}
info h4 {
font-size: 2rem;
}
:global(main) {
padding: 0px;
margin-bottom: 0px;
}
</style>
<OGPreview og={ og } twitter={ twitter } />
<Layout header={ false } footer={ false } fixed={ true } fullwidth={ true }>
<Hydrate component={ VideoDisplay } props={ { media } } />
</Layout>

2
static/color.css

@ -82,7 +82,7 @@
--width-card: 385px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
--width-content: 1280px;
--width-badge: 20px;
--font-size-badge: 13px;
--fixed-header-height: 73px;

11
static/global.css

@ -384,6 +384,8 @@ a i {
color: var(--color);
display: inline-block;
padding: 1rem 2rem;
width: 100%;
text-align: center;
}
button > a {
@ -1417,7 +1419,6 @@ sidebar {
sidebar a {
text-decoration: none;
padding: 0.5rem;
cursor: pointer;
}
@ -1455,3 +1456,11 @@ sidebar items {
sidebar items h3 {
padding-left: 0.3rem;
}
.no-select > * {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

Loading…
Cancel
Save