Added chapter support

This commit is contained in:
WardPearce
2024-03-22 17:34:37 +13:00
parent 64c337dd05
commit 27d5f46843
7 changed files with 144 additions and 43 deletions
+11
View File
@@ -91,6 +91,17 @@ export interface VideoPlay extends Video {
recommendedVideos: VideoBase[];
authorThumbnails: Image[];
captions: Captions[];
storyboards?: {
url: string;
templateUrl: string;
width: number;
height: number;
count: number;
interval: number;
storyboardWidth: number;
storyboardHeight: number;
storyboardCount: number;
}[];
}
export interface ReturnYTDislikes {
+5 -1
View File
@@ -1,5 +1,9 @@
<script lang="ts">
export let classes = '';
</script>
<svg
class="m l"
class={classes}
height="100%"
version="1.0"
viewBox="0 0 512 512"

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

+13 -1
View File
@@ -22,10 +22,18 @@
import { getDynamicTheme } from './theme';
export let data: { video: VideoPlay };
export let currentTime: number = 0;
export function seekTo(time: number) {
if (typeof player !== 'undefined') {
player.currentTime = time;
}
}
let player: Plyr | undefined = undefined;
let categoryBeingSkipped = '';
let player: Plyr | undefined;
let sponsorBlock: SponsorBlock | undefined;
let hls: Hls | undefined;
let dash: MediaPlayerClass | undefined;
@@ -85,6 +93,10 @@
}
});
player.on('timeupdate', (event: PlyrEvent) => {
currentTime = event.detail.plyr.currentTime;
});
player.autoplay = get(playerAutoPlay);
player.loop = get(playerAlwaysLoop);
+45
View File
@@ -11,4 +11,49 @@ export function numberWithCommas(number: number) {
export function cleanNumber(number: number): string {
return humanNumber(number, (number: number) => Number.parseFloat(number.toString()).toFixed(1));
}
export function phaseDescription(content: string): {
description: string, timestamps: { title: string; time: number; timePretty: string; }[];
} {
const timestamps: { title: string; time: number; timePretty: string; }[] = [];
const lines = content.split('\n');
const regex = /(\d+:\d+(?::\d+)?)(?:\s(.+))?/;
const filteredLines = lines.filter(line => {
const match = regex.exec(line);
if (match !== null) {
const timestamp = match[1];
const title = match[2] || '';
timestamps.push({
time: convertToSeconds(timestamp),
title: title,
timePretty: timestamp
});
return false;
}
return true;
});
const filteredContent = filteredLines.join('\n');
return { description: filteredContent, timestamps: timestamps };
}
function convertToSeconds(time: string): number {
const parts = time.split(':').map(part => parseInt(part));
let seconds = 0;
if (parts.length === 3) {
// hh:mm:ss
seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
} else if (parts.length === 2) {
// hh:ss or m:ss
seconds = parts[0] * 60 + parts[1];
} else if (parts.length === 1) {
// s
seconds = parts[0];
}
return seconds;
}
+1 -1
View File
@@ -219,7 +219,7 @@
><i>menu</i></button
>
<Logo />
<Logo classes="m l" />
<h6 class="m l">Materialious</h6>
<div class="max"></div>
@@ -4,10 +4,13 @@
import Player from '$lib/Player.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import { cleanNumber, numberWithCommas } from '$lib/misc.js';
import type Plyr from 'plyr';
import { activePage } from '../../../store.js';
export let data;
let player: Plyr | undefined;
activePage.set(null);
async function toggleSubscribed() {
@@ -19,13 +22,16 @@
data.subscribed = !data.subscribed;
}
let currentTime: number;
let seekTo: (time: number) => void;
</script>
{#if data}
<div class="grid">
<div class="s12 m12 l10">
{#key data.video.videoId}
<Player {data} />
<Player {data} bind:seekTo bind:currentTime />
{/key}
<h5>{data.video.title}</h5>
@@ -116,11 +122,22 @@
<p class="bold">
{numberWithCommas(data.video.viewCount)} views • {data.video.publishedText}
</p>
<p style="white-space: pre-line;word-wrap: break-word;">{data.video.description}</p>
<p style="white-space: pre-line;word-wrap: break-word;">{data.content.description}</p>
{#if data.content}
<h6 style="margin-bottom: .3em;">Chapters</h6>
{#each data.content.timestamps as timestamp}
<button
on:click={() => seekTo(timestamp.time)}
class="timestamps"
class:primary={timestamp.time <= currentTime}
>{timestamp.title} - {timestamp.timePretty}</button
>
{/each}
{/if}
</article>
<div class="space"></div>
{#if data.comments && data.comments.comments.length > 0}
{#if data.comments && typeof data.comments.comments !== 'undefined' && data.comments.comments.length > 0}
<h6>{numberWithCommas(data.comments.commentCount)} comments</h6>
{#each data.comments.comments as comment}
<div class="comment">
@@ -169,44 +186,54 @@
{/if}
</div>
<div class="s12 m12 l2">
{#each data.video.recommendedVideos as recommendedVideo}
<article class="no-padding">
<Thumbnail video={recommendedVideo} />
</article>
{/each}
{#if data.video.recommendedVideos}
{#each data.video.recommendedVideos as recommendedVideo}
<article class="no-padding">
<Thumbnail video={recommendedVideo} />
</article>
{/each}
{/if}
</div>
</div>
<style>
:root {
--plyr-color-main: var(--primary);
}
.comment {
display: flex;
}
.comment img {
margin: 0.5em 1em 0 1em;
}
.grid {
padding: 1em 10em;
}
.channel-owner {
background-color: var(--primary);
padding: 0 0.5em;
border-radius: 1em;
color: var(--surface-variant);
}
@media screen and (max-width: 1646px) {
.grid {
padding: 0;
}
}
</style>
{:else}
<PageLoading />
{/if}
<style>
:root {
--plyr-color-main: var(--primary);
}
.comment {
display: flex;
}
.comment img {
margin: 0.5em 1em 0 1em;
}
.grid {
padding: 1em 10em;
}
.channel-owner {
background-color: var(--primary);
padding: 0 0.5em;
border-radius: 1em;
color: var(--surface-variant);
}
@media screen and (max-width: 1646px) {
.grid {
padding: 0;
}
}
.timestamps {
margin-left: 0;
margin-bottom: 0.4em;
display: block;
background-color: var(--secondary-container);
color: var(--on-secondary-container);
}
</style>
@@ -1,4 +1,5 @@
import { amSubscribed, getComments, getDislikes, getVideo, postHistory } from '$lib/Api/index.js';
import { phaseDescription } from '$lib/misc';
import { get } from 'svelte/store';
import { auth, playerProxyVideos } from '../../../store';
@@ -11,6 +12,7 @@ export async function load({ params }) {
video: video,
returnYTDislikes: await getDislikes(params.slug),
comments: video.liveNow ? null : await getComments(params.slug, { sort_by: "top", source: "youtube" }),
subscribed: await amSubscribed(video.authorId)
subscribed: await amSubscribed(video.authorId),
content: phaseDescription(video.description)
};
};