Started transcript support
This commit is contained in:
Generated
+3
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -332,7 +332,6 @@
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
max-height: 160px;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user