Started transcript support

This commit is contained in:
WardPearce
2024-05-19 06:05:02 +12:00
parent d78a6780ca
commit 295850aaa0
9 changed files with 170 additions and 27 deletions
+3 -2
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.0.11",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.0.11",
"version": "1.1.0",
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor/android": "^6.0.0",
@@ -19,6 +19,7 @@
"human-number": "^2.0.4",
"iso-3166": "^4.3.0",
"material-dynamic-colors": "^1.1.0",
"media-captions": "^1.0.3",
"media-icons": "^0.10.0",
"mousetrap": "^1.6.5",
"peerjs": "^1.5.3",
+2 -1
View File
@@ -48,6 +48,7 @@
"human-number": "^2.0.4",
"iso-3166": "^4.3.0",
"material-dynamic-colors": "^1.1.0",
"media-captions": "^1.0.3",
"media-icons": "^0.10.0",
"mousetrap": "^1.6.5",
"peerjs": "^1.5.3",
@@ -57,4 +58,4 @@
"svelte-persisted-store": "^0.9.2",
"vidstack": "^1.11.21"
}
}
}
-6
View File
@@ -53,12 +53,6 @@
page.subscribe((pageUpdate) => loadTimeFromUrl(pageUpdate));
export function seekTo(time: number) {
if (typeof player !== 'undefined') {
player.currentTime = time;
}
}
const proxyVideos = get(playerProxyVideosStore);
onMount(async () => {
-1
View File
@@ -332,7 +332,6 @@
.thumbnail {
width: 100%;
overflow: hidden;
max-height: 160px;
}
.thumbnail img {
+139
View File
@@ -0,0 +1,139 @@
<script lang="ts">
import Fuse from 'fuse.js';
import { VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { MediaTimeUpdateEvent } from 'vidstack';
import type { MediaPlayerElement } from 'vidstack/elements';
import type { VideoPlay } from './Api/model';
import { videoLength } from './misc';
import { instanceStore } from './store';
export let video: VideoPlay;
export let player: MediaPlayerElement;
let url: string | null = null;
let transcript: ParsedCaptionsResult | null = null;
let transcriptCues: VTTCue[] = [];
let isLoading = false;
let currentTime = 0;
let search: string = '';
player.addEventListener('time-update', (event: MediaTimeUpdateEvent) => {
currentTime = event.detail.currentTime;
const currentTranscriptLine = document.querySelector(
'.transcript-line.secondary-container'
) as HTMLElement;
const transcriptScrollable = document.getElementById('transcript');
if (currentTranscriptLine && transcriptScrollable) {
transcriptScrollable.scrollTop =
currentTranscriptLine.offsetTop - transcriptScrollable.offsetTop - 300;
}
});
async function loadTranscript() {
if (!url) {
transcript = null;
return;
}
isLoading = true;
transcript = null;
const resp = await fetch(`${get(instanceStore)}${url}`);
transcript = await parseText(await resp.text(), { strict: false });
transcriptCues = transcript.cues;
isLoading = false;
}
function searchTranscript() {
if (!transcript) return;
if (search.trim() === '') {
transcriptCues = transcript.cues;
return;
}
const fuse = new Fuse(transcript.cues, {
keys: ['text']
});
transcriptCues = fuse.search(search).map((item) => item.item);
}
</script>
<article class="scroll no-padding" style="height: 75vh;" id="transcript">
<article class="no-elevate" style="position: sticky; top: 0; z-index: 3;">
<h6>{$_('transcript')}</h6>
<div class="field label suffix border">
<select bind:value={url} on:change={loadTranscript} name="captions">
<option selected={true} value={null}>{$_('selectLang')}</option>
{#each video.captions as caption}
<option value={caption.url}>{caption.label}</option>
{/each}
</select>
<label for="captions">{$_('language')}</label>
<i>arrow_drop_down</i>
</div>
<div class="max field round suffix prefix small no-margin surface-variant">
<i class="front">search</i><input
bind:value={search}
on:input={searchTranscript}
type="text"
placeholder={$_('searchPlaceholder')}
/>
</div>
<div class="space"></div>
<div class="divider"></div>
</article>
{#if isLoading}
<nav class="center-align">
<progress class="circle"></progress>
</nav>
{:else if transcript}
{#if transcript.cues.length > 0}
{#if transcriptCues.length > 0}
{#each transcriptCues as cue}
<div
class="transcript-line"
id={`transcript-line-${cue.startTime}`}
on:click={() => (player.currentTime = cue.startTime)}
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
>
<p class="chip no-margin">{videoLength(cue.startTime)}</p>
<p class="transcript-text">{cue.text}</p>
</div>
{/each}
{:else}
<nav class="center-align">
<h6>{$_('noResult')}</h6>
</nav>
{/if}
{:else}
<nav class="center-align">
<h6>{$_('noCaptionData')}</h6>
</nav>
{/if}
{/if}
</article>
<style>
.transcript-line {
display: flex;
cursor: pointer;
align-items: center;
padding: 0.5em;
margin: 1em;
}
.transcript-text {
margin-left: 1em;
}
.transcript-line .chip {
padding: 0.5em;
}
</style>
@@ -13,6 +13,11 @@
"skipping": "Skipping",
"replies": "replies",
"region": "Region",
"noResult": "No results",
"language": "Language",
"transcript": "Transcript",
"noCaptionData": "Captions don't have any data.",
"selectLang": "Select language",
"letterCase": "Letter case for titles",
"hideReplies": "Hide replies",
"hideVideo": "Hide video",
+1 -1
View File
@@ -75,7 +75,7 @@ export function cleanNumber(number: number): string {
export function videoLength(lengthSeconds: number): string {
const hours = Math.floor(lengthSeconds / 3600);
let minutes: number | string = Math.floor((lengthSeconds % 3600) / 60);
let seconds: number | string = lengthSeconds % 60;
let seconds: number | string = Math.round(lengthSeconds % 60);
if (minutes < 10) {
minutes = `0${minutes}`;
@@ -36,7 +36,7 @@
<div class="space"></div>
<div class="max field round suffix prefix small no-margin white black-text">
<div class="max field round suffix prefix small no-margin surface-variant">
<i class="front">search</i><input
bind:value={search}
on:input={searchSubs}
@@ -14,6 +14,7 @@
import Player from '$lib/Player.svelte';
import ShareVideo from '$lib/ShareVideo.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import Transcript from '$lib/Transcript.svelte';
import {
cleanNumber,
getBestThumbnail,
@@ -66,9 +67,10 @@
let theatreMode = get(playerTheatreModeByDefaultStore);
let audioMode = get(playerListenByDefaultStore);
let seekTo: (time: number) => void;
let player: MediaPlayerElement;
let showTranscript = false;
playlistSettingsStore.subscribe((playlistSetting) => {
if (!data.playlistId) return;
if (data.playlistId in playlistSetting) {
@@ -398,19 +400,13 @@
<div class="space"></div>
<div class="grid">
<div class={`s12 m12 l${theatreMode ? '12' : '10'}`}>
<div class={`s12 m12 l${theatreMode ? '12' : '9'}`}>
<div style="display: flex;justify-content: center;">
{#key data.video.videoId}
<div
style="max-height: 80vh;max-width: calc(80vh * 16 / 9);overflow: hidden;position: relative;flex: 1;"
>
<Player
{data}
{audioMode}
isSyncing={$syncPartyPeerStore !== null}
bind:seekTo
bind:player
/>
<Player {data} {audioMode} isSyncing={$syncPartyPeerStore !== null} bind:player />
</div>
{/key}
</div>
@@ -484,6 +480,15 @@
<i>width_wide</i>
<div class="tooltip">{$_('player.theatreMode')}</div>
</button>
<button
on:click={() => ((showTranscript = !showTranscript), (theatreMode = false))}
class:border={!showTranscript}
>
<i>description</i>
<div class="tooltip">
{$_('transcript')}
</div>
</button>
<button class="border"
><i>share</i>
<div class="tooltip">
@@ -568,7 +573,7 @@
{#if data.content.timestamps.length > 0}
<h6 style="margin-bottom: .3em;">Chapters</h6>
{#each data.content.timestamps as timestamp}
<button on:click={() => seekTo(timestamp.time)} class="timestamps"
<button on:click={() => (player.currentTime = timestamp.time)} class="timestamps"
>{timestamp.timePretty}
{#if !timestamp.title.startsWith('-')}
-
@@ -617,7 +622,10 @@
{/if}
</div>
{#if !theatreMode}
<div class="s12 m12 l2">
<div class="s12 m12 l3">
{#if player && showTranscript}
<Transcript bind:player video={data.video} />
{/if}
{#if !playlist}
{#if data.video.recommendedVideos}
{#each data.video.recommendedVideos as recommendedVideo}
@@ -700,10 +708,6 @@
--plyr-color-main: var(--primary);
}
.grid {
padding: 1em 10em;
}
.timestamps {
margin-left: 0;
margin-bottom: 0.4em;