Generated
+6
@@ -12,6 +12,7 @@
|
||||
"human-number": "^2.0.4",
|
||||
"media-icons": "^0.10.0",
|
||||
"sponsorblock-api": "^0.2.4",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-persisted-store": "^0.9.1",
|
||||
"vidstack": "^1.11.4"
|
||||
},
|
||||
@@ -7142,6 +7143,11 @@
|
||||
"svelte": "^3.19.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-infinite-loading": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.3.8.tgz",
|
||||
"integrity": "sha512-hn4o848LKd2Q+M11hiMWnfFxM1GHKVDi92HPZ1FYvfed4bEeRZL+QvFAQzhy1SACq6Si0CAJcQFUZpIYmAEnpQ=="
|
||||
},
|
||||
"node_modules/svelte-persisted-store": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-persisted-store/-/svelte-persisted-store-0.9.1.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"human-number": "^2.0.4",
|
||||
"media-icons": "^0.10.0",
|
||||
"sponsorblock-api": "^0.2.4",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-persisted-store": "^0.9.1",
|
||||
"vidstack": "^1.11.4"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<p style="margin-bottom: 0;">
|
||||
{comment.content}
|
||||
{@html comment.contentHtml}
|
||||
</p>
|
||||
<div style="display: flex;">
|
||||
<p><i>thumb_up</i> {numberWithCommas(comment.likeCount)}</p>
|
||||
|
||||
@@ -169,21 +169,9 @@
|
||||
|
||||
const currentTheme = await getDynamicTheme();
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
'--video-controls-color',
|
||||
currentTheme['--secondary']
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--audio-controls-color',
|
||||
currentTheme['--secondary']
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--audio-play-button-bg',
|
||||
currentTheme['--secondary']
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--media-slider-track-fill-bg',
|
||||
currentTheme['--secondary']
|
||||
currentTheme['--primary']
|
||||
);
|
||||
document.documentElement.style.setProperty('--media-menu-bg', currentTheme['--background']);
|
||||
document.documentElement.style.setProperty(
|
||||
@@ -191,12 +179,12 @@
|
||||
currentTheme['--surface']
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--media-menu-item-color',
|
||||
currentTheme['--primary-text']
|
||||
'--media-menu-text-color',
|
||||
currentTheme['--on-background']
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--media-menu-item-info-color',
|
||||
currentTheme['--primary-text']
|
||||
currentTheme['--on-background']
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
'--media-menu-section-bg',
|
||||
|
||||
@@ -38,7 +38,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<a class="wave" style="width: 100%; overflow: hidden;min-height:100px;" href={watchUrl}>
|
||||
<a
|
||||
class="wave"
|
||||
style="width: 100%; overflow: hidden;min-height:100px;"
|
||||
href={watchUrl}
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
{#if loading}
|
||||
<progress class="circle"></progress>
|
||||
{:else if loaded}
|
||||
|
||||
@@ -40,31 +40,46 @@ export interface PhasedDescription {
|
||||
|
||||
export function phaseDescription(content: string): PhasedDescription {
|
||||
const timestamps: { title: string; time: number; timePretty: string; }[] = [];
|
||||
console.log(content);
|
||||
const lines = content.split('\n');
|
||||
|
||||
const regex = /(\d+:\d+(?::\d+)?)(?:\s(.+))?/;
|
||||
const filteredLines = lines.filter(line => {
|
||||
const match = regex.exec(line);
|
||||
const urlRegex = /<a href="([^"]+)"/;
|
||||
const timestampRegex = /<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">(\d+:\d+(?::\d+)?)<\/a>\s*(.+)/;
|
||||
|
||||
if (match !== null) {
|
||||
const timestamp = match[1];
|
||||
const title = match[2] || '';
|
||||
timestamps.push({
|
||||
time: convertToSeconds(timestamp),
|
||||
title: title,
|
||||
timePretty: timestamp
|
||||
});
|
||||
return false;
|
||||
let filteredLines: string[] = [];
|
||||
lines.forEach(
|
||||
(line) => {
|
||||
const urlMatch = urlRegex.exec(line);
|
||||
const timestampMatch = timestampRegex.exec(line);
|
||||
|
||||
if (urlMatch !== null && timestampMatch === null) {
|
||||
// If line contains a URL but not a timestamp, modify the URL
|
||||
const modifiedLine = line.replace(/<a href="([^"]+)"/, '<a href="$1" target="_blank" rel="noopener noreferrer" class="link"');
|
||||
filteredLines.push(modifiedLine);
|
||||
} else if (timestampMatch !== null) {
|
||||
// If line contains a timestamp, extract details and push into timestamps array
|
||||
const time = timestampMatch[2];
|
||||
const timestamp = timestampMatch[3];
|
||||
const title = timestampMatch[4] || '';
|
||||
timestamps.push({
|
||||
time: parseInt(time),
|
||||
title: title,
|
||||
timePretty: timestamp
|
||||
});
|
||||
} else {
|
||||
filteredLines.push(line);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
@@ -535,7 +535,7 @@
|
||||
{/each}
|
||||
</dialog>
|
||||
|
||||
<main class="responsive max">
|
||||
<main class="responsive max root">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<script lang="ts">
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import { activePage } from '../store.js';
|
||||
import { activePage } from '../store';
|
||||
|
||||
export let data;
|
||||
|
||||
activePage.set('home');
|
||||
</script>
|
||||
|
||||
<VideoList videos={data.popular} />
|
||||
{#if data.popularDisabled}
|
||||
<div class="space"></div>
|
||||
|
||||
<nav class="center-align">
|
||||
<h3>Popular page has been disabled</h3>
|
||||
</nav>
|
||||
{:else}
|
||||
<VideoList videos={data.popular} />
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { getPopular } from '$lib/Api/index.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ params }) {
|
||||
export async function load() {
|
||||
let popular = undefined;
|
||||
let popularDisabled: boolean = false;
|
||||
|
||||
try {
|
||||
popular = await getPopular();
|
||||
} catch (errorMessage: any) {
|
||||
if (errorMessage.toString() === 'Error: Administrator has disabled this endpoint.') {
|
||||
popularDisabled = true;
|
||||
} else {
|
||||
error(500, errorMessage);
|
||||
}
|
||||
}
|
||||
return {
|
||||
popular: await getPopular()
|
||||
popular: popular,
|
||||
popularDisabled: popularDisabled
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
import PlaylistThumbnail from '$lib/PlaylistThumbnail.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { cleanNumber } from '$lib/misc';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
import { get } from 'svelte/store';
|
||||
import { activePage, auth } from '../../../store';
|
||||
|
||||
@@ -19,22 +20,29 @@
|
||||
|
||||
let displayContent: ChannelContentPlaylists | ChannelContentVideos | undefined = undefined;
|
||||
|
||||
async function handleScroll() {
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
if (typeof displayContent === 'undefined') return;
|
||||
|
||||
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 5) {
|
||||
const newContent = await getChannelContent(data.channel.authorId, {
|
||||
type: tab,
|
||||
continuation: displayContent.continuation
|
||||
});
|
||||
if ('videos' in newContent && 'videos' in displayContent) {
|
||||
displayContent.videos = [...displayContent.videos, ...newContent.videos];
|
||||
} else if ('playlists' in displayContent && 'playlists' in newContent) {
|
||||
displayContent.playlists = [...displayContent.playlists, ...newContent.playlists];
|
||||
const newContent = await getChannelContent(data.channel.authorId, {
|
||||
type: tab,
|
||||
continuation: displayContent.continuation
|
||||
});
|
||||
if ('videos' in newContent && 'videos' in displayContent) {
|
||||
if (displayContent.continuation === newContent.continuation) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
event.detail.loaded();
|
||||
}
|
||||
displayContent.continuation = newContent.continuation;
|
||||
displayContent.videos = [...displayContent.videos, ...newContent.videos];
|
||||
} else if ('playlists' in displayContent && 'playlists' in newContent) {
|
||||
if (displayContent.continuation === newContent.continuation) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
event.detail.loaded();
|
||||
}
|
||||
displayContent.playlists = [...displayContent.playlists, ...newContent.playlists];
|
||||
}
|
||||
displayContent.continuation = newContent.continuation;
|
||||
}
|
||||
|
||||
async function changeTab(newTab: 'videos' | 'playlists' | 'streams' | 'shorts') {
|
||||
@@ -49,12 +57,6 @@
|
||||
if (get(auth)) {
|
||||
isSubscribed = await amSubscribed(data.channel.authorId);
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
async function toggleSubscribed() {
|
||||
@@ -144,6 +146,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
{:else}
|
||||
<PageLoading />
|
||||
{/if}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import type { VideoPlay } from '$lib/Api/model';
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
import { activePage } from '../../store';
|
||||
|
||||
activePage.set('history');
|
||||
@@ -28,23 +29,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScroll() {
|
||||
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 5) {
|
||||
currentPage += 1;
|
||||
await loadPageHistory();
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
const pastHistoryLen = Number(history.length);
|
||||
|
||||
await loadPageHistory();
|
||||
|
||||
if (pastHistoryLen === history.length) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
event.detail.loaded();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadPageHistory();
|
||||
loaded = true;
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -66,3 +65,5 @@
|
||||
</div>
|
||||
|
||||
<VideoList videos={history} />
|
||||
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import { cleanNumber } from '$lib/misc.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { activePage } from '../../../store.js';
|
||||
import { activePage } from '../../../store';
|
||||
|
||||
export let data;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import ChannelThumbnail from '$lib/ChannelThumbnail.svelte';
|
||||
import PlaylistThumbnail from '$lib/PlaylistThumbnail.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
import { activePage } from '../../../store';
|
||||
|
||||
export let data;
|
||||
@@ -20,24 +20,20 @@
|
||||
document.location.href = $page.url.href;
|
||||
}
|
||||
|
||||
async function handleScroll() {
|
||||
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 5) {
|
||||
currentPage += 1;
|
||||
search = [
|
||||
...search,
|
||||
...(await getSearch(data.slug, { page: currentPage.toString(), type: data.searchType }))
|
||||
];
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
currentPage++;
|
||||
const newSearch = await getSearch(data.slug, {
|
||||
page: currentPage.toString(),
|
||||
type: data.searchType
|
||||
});
|
||||
|
||||
if (newSearch.length === 0) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
search = [...search, ...newSearch];
|
||||
event.detail.loaded();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space" style="margin-bottom: 1em;">
|
||||
@@ -91,3 +87,5 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { getFeed } from '$lib/Api/index.js';
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { activePage } from '../../store.js';
|
||||
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
|
||||
import { activePage } from '../../store';
|
||||
|
||||
export let data;
|
||||
|
||||
let currentPage = 1;
|
||||
let videos = [...data.feed.videos, ...data.feed.notifications];
|
||||
$: videos = [...data.feed.videos, ...data.feed.notifications];
|
||||
|
||||
activePage.set('subscriptions');
|
||||
|
||||
async function handleScroll() {
|
||||
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 5) {
|
||||
currentPage += 1;
|
||||
const feed = await getFeed(100, currentPage);
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
currentPage++;
|
||||
const feed = await getFeed(100, currentPage);
|
||||
if (feed.notifications.length === 0) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
videos = [...videos, ...feed.notifications];
|
||||
event.detail.loaded();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
|
||||
<VideoList bind:videos />
|
||||
|
||||
<InfiniteLoading on:infinite={loadMore} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import { activePage } from '../../store.js';
|
||||
import { activePage } from '../../store';
|
||||
|
||||
export let data;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { cleanNumber, numberWithCommas } from '$lib/misc.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { activePage, auth, playerListenByDefault } from '../../../store.js';
|
||||
import { activePage, auth, playerListenByDefault } from '../../../store';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -213,28 +213,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="medium scroll">
|
||||
<p class="bold">
|
||||
{numberWithCommas(data.video.viewCount)} views • {data.video.publishedText}
|
||||
</p>
|
||||
<p style="white-space: pre-line;word-wrap: break-word;">{data.content.description}</p>
|
||||
{#if data.content}
|
||||
{#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"
|
||||
class:primary={timestamp.time <= currentTime}
|
||||
>{timestamp.timePretty}
|
||||
{#if !timestamp.title.startsWith('-')}
|
||||
-
|
||||
{/if}
|
||||
{timestamp.title}</button
|
||||
>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
<article>
|
||||
<details>
|
||||
<summary class="bold none">
|
||||
<nav>
|
||||
<div class="max">
|
||||
{numberWithCommas(data.video.viewCount)} views • {data.video.publishedText}
|
||||
</div>
|
||||
<i>expand_more</i>
|
||||
</nav>
|
||||
</summary>
|
||||
<div class="space"></div>
|
||||
<div class="medium scroll">
|
||||
<div style="white-space: pre-line; overflow-wrap: break-word;">
|
||||
{@html data.content.description}
|
||||
</div>
|
||||
{#if data.content}
|
||||
{#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"
|
||||
class:primary={timestamp.time <= currentTime}
|
||||
>{timestamp.timePretty}
|
||||
{#if !timestamp.title.startsWith('-')}
|
||||
-
|
||||
{/if}
|
||||
{timestamp.title}</button
|
||||
>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
|
||||
<div class="space"></div>
|
||||
@@ -247,7 +259,7 @@
|
||||
<button on:click={loadMoreComments} class="margin">Load more</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<h6>Comments disabled</h6>
|
||||
<h6>Unable to load comments</h6>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="s12 m12 l2">
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function load({ params, url }) {
|
||||
returnYTDislikes: returnYTDislikes,
|
||||
comments: comments,
|
||||
subscribed: await amSubscribed(video.authorId),
|
||||
content: phaseDescription(video.description),
|
||||
content: phaseDescription(video.descriptionHtml),
|
||||
playlistId: url.searchParams.get('playlist'),
|
||||
personalPlaylists: personalPlaylists
|
||||
};
|
||||
|
||||
@@ -7,4 +7,9 @@
|
||||
padding-left: .1em !important;
|
||||
padding-right: .1em !important;
|
||||
}
|
||||
}
|
||||
|
||||
main.root {
|
||||
max-height: 100vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
Reference in New Issue
Block a user