Improved share component
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { shareURL } from '$lib/misc';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { invidiousInstanceStore } from '$lib/store';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { titleCase } from '$lib/letterCasing';
|
||||
|
||||
type ShareLink = {
|
||||
type: 'invidious' | 'youtube' | 'materialious' | 'invidious redirect';
|
||||
path: string;
|
||||
param?: {
|
||||
key: string;
|
||||
value: () => string | number;
|
||||
};
|
||||
};
|
||||
|
||||
let {
|
||||
shares,
|
||||
includePromptText = undefined,
|
||||
iconOnly = true
|
||||
}: {
|
||||
shares: ShareLink[];
|
||||
includePromptText?: string;
|
||||
iconOnly: boolean;
|
||||
} = $props();
|
||||
|
||||
const shareBase = {
|
||||
invidious: $invidiousInstanceStore,
|
||||
youtube: 'https://www.youtube.com',
|
||||
materialious: !Capacitor.isNativePlatform() ? location.origin : undefined,
|
||||
'invidious redirect': 'https://redirect.invidious.io'
|
||||
};
|
||||
|
||||
let includePrompt = $state(false);
|
||||
|
||||
async function onShare(share: ShareLink) {
|
||||
const url = new URL(`${shareBase[share.type]}${share.path}`);
|
||||
|
||||
if (share.param && includePrompt)
|
||||
url.searchParams.append(share.param.key, share.param.value().toString());
|
||||
|
||||
await shareURL(url.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="surface-container-highest" onclick={(event: Event) => event.stopPropagation()}>
|
||||
<i>share</i>
|
||||
{#if !iconOnly}
|
||||
{$_('player.share.title')}
|
||||
{/if}
|
||||
<div class="tooltip">
|
||||
{$_('player.share.title')}
|
||||
</div>
|
||||
<menu class="no-wrap mobile" data-ui="#share-menu" id="share-menu">
|
||||
{#if includePromptText}
|
||||
<li class="row">
|
||||
<label class="switch">
|
||||
<input type="checkbox" bind:checked={includePrompt} />
|
||||
<span></span>
|
||||
</label>
|
||||
<div class="min">{includePromptText}</div>
|
||||
</li>
|
||||
<div class="divider"></div>
|
||||
{/if}
|
||||
|
||||
{#each shares as share (share)}
|
||||
{#if shareBase[share.type]}
|
||||
<li
|
||||
data-ui="#share-menu"
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={() => {
|
||||
onShare(share);
|
||||
}}
|
||||
>
|
||||
<div class="min">
|
||||
{$_('player.share.copyXLink', {
|
||||
linkType: titleCase(share.type)
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</menu>
|
||||
</button>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { invidiousInstanceStore } from '$lib/store';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model';
|
||||
import { isUnrestrictedPlatform, shareURL } from '$lib/misc';
|
||||
import { addToast } from './Toast.svelte';
|
||||
|
||||
interface Props {
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
currentTime?: number;
|
||||
}
|
||||
|
||||
let { video, currentTime = $bindable() }: Props = $props();
|
||||
let includeTimestamp: boolean = $state(false);
|
||||
|
||||
async function shareVideo(url: string, param: string = 't') {
|
||||
if (includeTimestamp) url += `?${param}=${Math.floor(currentTime ?? 0)}`;
|
||||
|
||||
await shareURL(url);
|
||||
|
||||
addToast({
|
||||
data: {
|
||||
text: $_('player.share.copiedSuccess')
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<menu class="no-wrap mobile" data-ui="#share-menu" id="share-menu">
|
||||
{#if currentTime !== undefined}
|
||||
<li class="row">
|
||||
<label class="switch">
|
||||
<input type="checkbox" bind:checked={includeTimestamp} />
|
||||
<span></span>
|
||||
</label>
|
||||
<div class="min">{$_('player.share.includeTimestamp')}</div>
|
||||
</li>
|
||||
<div class="divider"></div>
|
||||
{/if}
|
||||
<li
|
||||
data-ui="#share-menu"
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
if (isUnrestrictedPlatform()) {
|
||||
shareVideo(`${get(invidiousInstanceStore)}/watch/${video.videoId}`);
|
||||
} else {
|
||||
shareVideo(
|
||||
`${location.origin}${resolve('/watch/[videoId]', { videoId: video.videoId })}`,
|
||||
'time'
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="min">{$_('player.share.materialiousLink')}</div>
|
||||
</li>
|
||||
<li
|
||||
data-ui="#share-menu"
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
shareVideo(`https://redirect.invidious.io/watch?v=${video.videoId}`);
|
||||
}}
|
||||
>
|
||||
<div class="min">{$_('player.share.invidiousRedirect')}</div>
|
||||
</li>
|
||||
<li
|
||||
data-ui="#share-menu"
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
shareVideo(`https://www.youtube.com/watch?v=${video.videoId}`);
|
||||
}}
|
||||
>
|
||||
<div class="min">{$_('player.share.youtubeLink')}</div>
|
||||
</li>
|
||||
</menu>
|
||||
@@ -16,7 +16,7 @@
|
||||
{#if playerTextTracks && playerTextTracks.length > 0 && !video.liveNow}
|
||||
<button class="surface-container-highest">
|
||||
<i>closed_caption</i>
|
||||
<menu class="no-wrap mobile" id="cc-menu" data-ui="#cc-menu">
|
||||
<menu class="no-wrap mobile player-settings" id="cc-menu" data-ui="#cc-menu">
|
||||
<li
|
||||
role="presentation"
|
||||
data-ui="#cc-menu"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import type { PlaylistPage, PlaylistPageVideo, VideoPlay } from '$lib/api/model';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { goToNextVideo, goToPreviousVideo } from '$lib/player';
|
||||
import { playlistSettingsStore } from '$lib/store';
|
||||
import VideoThumbnail from '../thumbnail/VideoThumbnail.svelte';
|
||||
|
||||
let {
|
||||
playlist,
|
||||
video
|
||||
}: { playlist: { videos: PlaylistPageVideo[]; info: PlaylistPage }; video: VideoPlay } = $props();
|
||||
|
||||
let loopPlaylist: boolean = $state(false);
|
||||
let shufflePlaylist: boolean = $state(false);
|
||||
|
||||
playlistSettingsStore.subscribe((playlistSetting) => {
|
||||
if (playlist.info.playlistId in playlistSetting) {
|
||||
loopPlaylist = playlistSetting[playlist.info.playlistId].loop;
|
||||
shufflePlaylist = playlistSetting[playlist.info.playlistId].shuffle;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<article
|
||||
style="height: 85vh; position: relative;scrollbar-width: none;"
|
||||
id="playlist"
|
||||
class="scroll no-padding surface-container border"
|
||||
>
|
||||
<article class="no-elevate border" style="position: sticky; top: 0; z-index: 3;">
|
||||
<h6>{playlist.info.title}</h6>
|
||||
<p>
|
||||
{cleanNumber(playlist.info.viewCount)}
|
||||
{$_('views')} • {playlist.info.videoCount}
|
||||
{$_('videos')}
|
||||
</p>
|
||||
<p>
|
||||
{#if playlist.info.authorId}
|
||||
<a
|
||||
href={resolve(`/channel/[authorId]`, {
|
||||
authorId: playlist.info.authorId
|
||||
})}>{playlist.info.author}</a
|
||||
>
|
||||
{:else}
|
||||
{playlist.info.author}
|
||||
{/if}
|
||||
</p>
|
||||
<nav>
|
||||
<button
|
||||
class="circle surface-container-highest"
|
||||
onclick={() => goToPreviousVideo(playlist.info.playlistId)}
|
||||
>
|
||||
<i>skip_previous</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.previous')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
loopPlaylist = !loopPlaylist;
|
||||
playlistSettingsStore.set({
|
||||
[playlist.info.playlistId]: { loop: loopPlaylist, shuffle: shufflePlaylist }
|
||||
});
|
||||
}}
|
||||
class="circle"
|
||||
class:surface-container-highest={!loopPlaylist}
|
||||
>
|
||||
<i>loop</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.loopPlaylist')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
shufflePlaylist = !shufflePlaylist;
|
||||
playlistSettingsStore.set({
|
||||
[playlist.info.playlistId]: { loop: loopPlaylist, shuffle: shufflePlaylist }
|
||||
});
|
||||
}}
|
||||
class="circle"
|
||||
class:surface-container-highest={!shufflePlaylist}
|
||||
>
|
||||
<i>shuffle</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.shuffleVideos')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="circle surface-container-highest"
|
||||
onclick={async () => await goToNextVideo(video, playlist.info.playlistId)}
|
||||
>
|
||||
<i>skip_next</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.next')}
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="divider"></div>
|
||||
</article>
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
{#each playlist.videos as playlistVideo, index (index)}
|
||||
<article
|
||||
class="no-padding border"
|
||||
style="margin: .7em;"
|
||||
id={playlistVideo.videoId}
|
||||
class:primary-border={playlistVideo.videoId === video.videoId}
|
||||
>
|
||||
{#key playlistVideo.videoId}
|
||||
<VideoThumbnail
|
||||
video={playlistVideo}
|
||||
sideways={true}
|
||||
playlistId={playlist.info.playlistId || undefined}
|
||||
/>
|
||||
{/key}
|
||||
</article>
|
||||
{/each}
|
||||
</article>
|
||||
@@ -86,7 +86,7 @@
|
||||
<article class="scroll border no-padding" style="height: 75vh;" id="transcript">
|
||||
<article class="no-elevate padding" style="position: sticky; top: 0; z-index: 3;">
|
||||
<h6>{$_('transcript')}</h6>
|
||||
<div class="field label suffix border">
|
||||
<div class="field label suffix surface-container-highest">
|
||||
<select bind:value={url} onchange={loadTranscript} name="captions">
|
||||
<option selected={true} value={null}>{$_('selectLang')}</option>
|
||||
{#each video.captions as caption (caption)}
|
||||
@@ -97,7 +97,8 @@
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
{#if transcriptCues.length > 0}
|
||||
<div class="max field round suffix prefix small no-margin surface-variant">
|
||||
<div class="space"></div>
|
||||
<div class="max field suffix prefix small no-margin surface-container-highest">
|
||||
<i class="front">search</i><input
|
||||
bind:value={search}
|
||||
oninput={searchTranscript}
|
||||
@@ -127,7 +128,8 @@
|
||||
class="transcript-line"
|
||||
role="presentation"
|
||||
onclick={() => (playerElement.currentTime = cue.startTime)}
|
||||
class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime}
|
||||
class:surface-container-highest={currentTime >= cue.startTime &&
|
||||
currentTime <= cue.endTime}
|
||||
>
|
||||
<p class="chip no-margin">{videoLength(cue.startTime)}</p>
|
||||
<p class="transcript-text">{decodeHtmlCharCodes(cue.text.replace(/<[^>]+>/g, ''))}</p>
|
||||
|
||||
@@ -126,9 +126,7 @@
|
||||
"chapters": "Chapters",
|
||||
"share": {
|
||||
"title": "Share",
|
||||
"materialiousLink": "Copy Materialious link",
|
||||
"invidiousRedirect": "Copy Invidious redirect link",
|
||||
"youtubeLink": "Copy Youtube link",
|
||||
"copyXLink": "Copy {{linkType}} link",
|
||||
"includeTimestamp": "Timestamped",
|
||||
"copiedSuccess": "Copied to clipboard"
|
||||
},
|
||||
|
||||
@@ -33,6 +33,8 @@ import { Clipboard } from '@capacitor/clipboard';
|
||||
import { isOwnBackend } from './shared';
|
||||
import { Browser } from '@capacitor/browser';
|
||||
import { clearFeedYTjs } from './api/youtubejs/subscriptions';
|
||||
import { addToast } from './components/Toast.svelte';
|
||||
import { _ } from './i18n';
|
||||
|
||||
export function getPublicEnv(envName: string): string | undefined {
|
||||
return env[`PUBLIC_${envName}`] ?? import.meta.env[`VITE_${envName}`];
|
||||
@@ -114,6 +116,12 @@ export async function shareURL(url: string) {
|
||||
} else {
|
||||
await Clipboard.write({ string: url });
|
||||
}
|
||||
|
||||
addToast({
|
||||
data: {
|
||||
text: get(_)('player.share.copiedSuccess')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureNoTrailingSlash(url: any): string {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
playerDefaultLanguage,
|
||||
playerDefaultQualityStore,
|
||||
playerPlaylistHistory,
|
||||
playerState,
|
||||
playlistSettingsStore,
|
||||
syncPartyConnectionsStore
|
||||
} from '$lib/store';
|
||||
@@ -34,6 +35,8 @@ export const playbackRates = [
|
||||
export const playerDoubleTapSeek = 10.0;
|
||||
|
||||
export function goToPreviousVideo(playlistId: string | null) {
|
||||
playerState.set(undefined);
|
||||
|
||||
const previousVideos = get(playerPlaylistHistory);
|
||||
if (previousVideos.length > 1) {
|
||||
goto(
|
||||
@@ -47,6 +50,8 @@ export function goToPreviousVideo(playlistId: string | null) {
|
||||
}
|
||||
|
||||
export async function goToNextVideo(video: VideoPlay, playlistId: string | null) {
|
||||
playerState.set(undefined);
|
||||
|
||||
const isAndroidTv = get(isAndroidTvStore);
|
||||
|
||||
if (!playlistId) {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { isYTBackend } from '$lib/misc';
|
||||
import Share from '$lib/components/Share.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let tab: ChannelContentTypes = $state('videos');
|
||||
|
||||
@@ -108,37 +110,29 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if !$isAndroidTvStore}
|
||||
<button class="border">
|
||||
<i>share</i>
|
||||
<span>{$_('player.share.title')}</span>
|
||||
<menu class="no-wrap mobile">
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<li
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
await Clipboard.write({ string: location.href });
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
}}
|
||||
>
|
||||
{$_('player.share.materialiousLink')}
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
<li
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
await Clipboard.write({
|
||||
string: `https://www.youtube.com/channel/${page.params.slug}`
|
||||
});
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
}}
|
||||
>
|
||||
{$_('player.share.youtubeLink')}
|
||||
</li>
|
||||
</menu>
|
||||
</button>
|
||||
<Share
|
||||
iconOnly={false}
|
||||
shares={[
|
||||
{
|
||||
type: 'materialious',
|
||||
path: resolve('/channel/[channelId]', {
|
||||
channelId: page.params.slug
|
||||
})
|
||||
},
|
||||
{
|
||||
type: 'youtube',
|
||||
path: `/channel/${page.params.slug}`
|
||||
},
|
||||
{
|
||||
type: 'invidious',
|
||||
path: `/channel/${page.params.slug}`
|
||||
},
|
||||
{
|
||||
type: 'invidious redirect',
|
||||
path: `/channel/${page.params.slug}`
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { unsafeRandomItem } from '$lib/misc';
|
||||
import { cleanNumber } from '$lib/numbers';
|
||||
import { isAndroidTvStore, playlistSettingsStore } from '$lib/store';
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { _ } from '$lib/i18n';
|
||||
import ItemsList from '$lib/components/layout/ItemsList.svelte';
|
||||
import Share from '$lib/components/Share.svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
@@ -44,7 +44,7 @@
|
||||
playlistSettingsStore.set({
|
||||
[data.playlist.info.playlistId]: { shuffle: true, loop: false }
|
||||
})}
|
||||
class="button circle extra no-margin border"
|
||||
class="button circle extra no-margin surface-container-highest"
|
||||
>
|
||||
<i>shuffle</i>
|
||||
<div class="tooltip bottom">
|
||||
@@ -68,36 +68,29 @@
|
||||
<div class="space"></div>
|
||||
|
||||
{#if !$isAndroidTvStore}
|
||||
<button class="border no-margin">
|
||||
<i>share</i>
|
||||
<span>{$_('player.share.title')}</span>
|
||||
<menu class="no-wrap mobile">
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<li
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
await Clipboard.write({ string: location.href });
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
}}
|
||||
>
|
||||
{$_('player.share.materialiousLink')}
|
||||
</li>
|
||||
{/if}
|
||||
<li
|
||||
class="row"
|
||||
role="presentation"
|
||||
onclick={async () => {
|
||||
await Clipboard.write({
|
||||
string: `https://www.youtube.com/playlist?list=${data.playlist.info.playlistId}`
|
||||
});
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
}}
|
||||
>
|
||||
{$_('player.share.youtubeLink')}
|
||||
</li>
|
||||
</menu>
|
||||
</button>
|
||||
<nav class="right-align">
|
||||
<Share
|
||||
iconOnly={false}
|
||||
shares={[
|
||||
{
|
||||
type: 'materialious',
|
||||
path: resolve('/playlist/[playlistId]', { playlistId: page.params.slug })
|
||||
},
|
||||
{
|
||||
type: 'invidious',
|
||||
path: `/playlist?list=${page.params.slug}`
|
||||
},
|
||||
{
|
||||
type: 'invidious redirect',
|
||||
path: `/playlist?list=${page.params.slug}`
|
||||
},
|
||||
{
|
||||
type: 'youtube',
|
||||
path: `/playlist?list=${page.params.slug}`
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</nav>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
removePlaylistVideo
|
||||
} from '$lib/api/index';
|
||||
import type { Comments, PlaylistPage } from '$lib/api/model';
|
||||
import ShareVideo from '$lib/components/ShareVideo.svelte';
|
||||
import Thumbnail from '$lib/components/thumbnail/VideoThumbnail.svelte';
|
||||
import Transcript from '$lib/components/watch/Transcript.svelte';
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
@@ -42,6 +41,8 @@
|
||||
import { humanizeSeconds, relativeTimestamp } from '$lib/time';
|
||||
import { getWatchDetails } from '$lib/watch';
|
||||
import { page } from '$app/state';
|
||||
import Share from '$lib/components/Share.svelte';
|
||||
import Playlist from '$lib/components/watch/Playlist.svelte';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
@@ -55,9 +56,6 @@
|
||||
let personalPlaylists: PlaylistPage[] | null = $state(null);
|
||||
data.streamed.personalPlaylists?.then((streamPlaylists) => (personalPlaylists = streamPlaylists));
|
||||
|
||||
let loopPlaylist: boolean = $state(false);
|
||||
let shufflePlaylist: boolean = $state(false);
|
||||
|
||||
playertheatreModeIsActive.set(get(playerTheatreModeByDefaultStore));
|
||||
|
||||
let pauseTimerSeconds: number = $state(-1);
|
||||
@@ -84,14 +82,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
playlistSettingsStore.subscribe((playlistSetting) => {
|
||||
if (!data.playlistId) return;
|
||||
if (data.playlistId in playlistSetting) {
|
||||
loopPlaylist = playlistSetting[data.playlistId].loop;
|
||||
shufflePlaylist = playlistSetting[data.playlistId].shuffle;
|
||||
}
|
||||
});
|
||||
|
||||
function playerSyncEvents(conn: DataConnection) {
|
||||
if (playerElement) {
|
||||
conn.send({
|
||||
@@ -449,15 +439,44 @@
|
||||
{$_('transcript')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="surface-container-highest"
|
||||
onclick={(event: Event) => event.stopPropagation()}
|
||||
><i>share</i>
|
||||
<div class="tooltip">
|
||||
{$_('player.share.title')}
|
||||
</div>
|
||||
<ShareVideo bind:currentTime={playerCurrentTime} video={data.video} />
|
||||
</button>
|
||||
<Share
|
||||
includePromptText={$_('player.share.includeTimestamp')}
|
||||
shares={[
|
||||
{
|
||||
type: 'materialious',
|
||||
path: resolve('/watch/[videoId]', { videoId: data.video.videoId }),
|
||||
param: {
|
||||
key: 'time',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'invidious',
|
||||
path: `/watch?=${data.video.videoId}`,
|
||||
param: {
|
||||
key: 'v',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'invidious redirect',
|
||||
path: `/watch?=${data.video.videoId}`,
|
||||
param: {
|
||||
key: 'v',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'youtube',
|
||||
path: `/watch?=${data.video.videoId}`,
|
||||
param: {
|
||||
key: 'v',
|
||||
value: () => Math.round(playerCurrentTime)
|
||||
}
|
||||
}
|
||||
]}
|
||||
iconOnly={true}
|
||||
/>
|
||||
{#if personalPlaylists && personalPlaylists.length > 0}
|
||||
<button class="surface-container-highest">
|
||||
<i>add</i>
|
||||
@@ -581,100 +600,7 @@
|
||||
<Transcript video={data.video} bind:playerElement />
|
||||
{/if}
|
||||
{#if data.playlistId && data.playlistId in $playlistCacheStore}
|
||||
<article
|
||||
style="height: 85vh; position: relative;scrollbar-width: none;"
|
||||
id="playlist"
|
||||
class="scroll no-padding surface-container border"
|
||||
>
|
||||
<article class="no-elevate border" style="position: sticky; top: 0; z-index: 3;">
|
||||
<h6>{$playlistCacheStore[data.playlistId].info.title}</h6>
|
||||
<p>
|
||||
{cleanNumber($playlistCacheStore[data.playlistId].info.viewCount)}
|
||||
{$_('views')} • {$playlistCacheStore[data.playlistId].info.videoCount}
|
||||
{$_('videos')}
|
||||
</p>
|
||||
<p>
|
||||
{#if $playlistCacheStore[data.playlistId].info.authorId}
|
||||
<a
|
||||
href={resolve(`/channel/[authorId]`, {
|
||||
authorId: $playlistCacheStore[data.playlistId].info.authorId
|
||||
})}>{$playlistCacheStore[data.playlistId].info.author}</a
|
||||
>
|
||||
{:else}
|
||||
{$playlistCacheStore[data.playlistId].info.author}
|
||||
{/if}
|
||||
</p>
|
||||
<nav>
|
||||
<button
|
||||
onclick={() => {
|
||||
loopPlaylist = !loopPlaylist;
|
||||
playlistSettingsStore.set({
|
||||
[data.playlistId as string]: { loop: loopPlaylist, shuffle: shufflePlaylist }
|
||||
});
|
||||
}}
|
||||
class="circle"
|
||||
class:fill={!loopPlaylist}
|
||||
>
|
||||
<i>loop</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.loopPlaylist')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
shufflePlaylist = !shufflePlaylist;
|
||||
playlistSettingsStore.set({
|
||||
[data.playlistId as string]: { loop: loopPlaylist, shuffle: shufflePlaylist }
|
||||
});
|
||||
}}
|
||||
class="circle"
|
||||
class:fill={!shufflePlaylist}
|
||||
>
|
||||
<i>shuffle</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.shuffleVideos')}
|
||||
</div>
|
||||
</button>
|
||||
<button class="circle fill" onclick={() => goToPreviousVideo(data.playlistId)}>
|
||||
<i>skip_previous</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.previous')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="circle fill"
|
||||
onclick={async () => await goToNextVideo(data.video, data.playlistId)}
|
||||
>
|
||||
<i>skip_next</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.next')}
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="divider"></div>
|
||||
</article>
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
{#each $playlistCacheStore[data.playlistId].videos as playlistVideo (playlistVideo.videoId)}
|
||||
<article
|
||||
class="no-padding border"
|
||||
style="margin: .7em;"
|
||||
id={playlistVideo.videoId}
|
||||
class:primary-border={playlistVideo.videoId === data.video.videoId}
|
||||
>
|
||||
{#key playlistVideo.videoId}
|
||||
<Thumbnail
|
||||
video={playlistVideo}
|
||||
sideways={true}
|
||||
playlistId={data.playlistId || undefined}
|
||||
/>
|
||||
{/key}
|
||||
</article>
|
||||
{/each}
|
||||
</article>
|
||||
<Playlist video={data.video} playlist={$playlistCacheStore[data.playlistId]} />
|
||||
{:else if data.video.recommendedVideos}
|
||||
{#each data.video.recommendedVideos as recommendedVideo (recommendedVideo.videoId)}
|
||||
<article class="no-padding border">
|
||||
|
||||
Reference in New Issue
Block a user