Merge pull request #104 from WardPearce/update/playlist-minor-syling
Update/playlist minor syling
This commit is contained in:
Vendored
+2
-1
@@ -7,5 +7,6 @@
|
||||
"materialious/src/lib/i18n",
|
||||
"materialious/src/lib/i18n/locales"
|
||||
],
|
||||
"i18n-ally.extract.autoDetect": true
|
||||
"i18n-ally.extract.autoDetect": true,
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
@@ -69,7 +69,9 @@
|
||||
><div class="bold">{truncate(playlist.title)}</div></a
|
||||
>
|
||||
<div>
|
||||
<a href={`/channel/${playlist.authorId}`}>{playlist.author}</a>
|
||||
<a class:link-disabled={playlist.authorId === null} href={`/channel/${playlist.authorId}`}
|
||||
>{playlist.author}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -16,11 +16,15 @@
|
||||
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
export let playlistId: string = '';
|
||||
|
||||
let watchUrl = `/watch/${video.videoId}` + (playlistId ? `?playlist=${playlistId}` : '');
|
||||
let watchUrl = new URL(`${import.meta.env.VITE_DEFAULT_FRONTEND_URL}/watch/${video.videoId}`);
|
||||
|
||||
if (playlistId !== '') {
|
||||
watchUrl.searchParams.set('playlist', playlistId);
|
||||
}
|
||||
|
||||
syncPartyPeer.subscribe((peer) => {
|
||||
if (peer) {
|
||||
watchUrl += (playlistId ? '&' : '?') + `sync=${peer.id}`;
|
||||
watchUrl.searchParams.set('sync', peer.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -140,7 +144,7 @@
|
||||
<a
|
||||
class="wave"
|
||||
style="width: 100%; overflow: hidden;min-height:100px;"
|
||||
href={watchUrl}
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
on:click={syncChangeVideo}
|
||||
>
|
||||
@@ -179,7 +183,7 @@
|
||||
<div class="small-padding">
|
||||
<nav class="no-margin">
|
||||
<div class="max">
|
||||
<a href={watchUrl}><div class="bold">{truncate(video.title)}</div></a>
|
||||
<a href={watchUrl.toString()}><div class="bold">{truncate(video.title)}</div></a>
|
||||
<div>
|
||||
<a href={`/channel/${video.authorId}`}>{video.author}</a
|
||||
>{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
"createPlaylist": "Create playlist",
|
||||
"public": "Public",
|
||||
"unlisted": "Unlisted",
|
||||
"private": "Private"
|
||||
"private": "Private",
|
||||
"playVideos": "Play videos",
|
||||
"shuffleVideos": "Play videos shuffled",
|
||||
"loopPlaylist": "Loop playlist"
|
||||
},
|
||||
"thumbnail": {
|
||||
"live": "LIVE",
|
||||
@@ -107,4 +110,4 @@
|
||||
},
|
||||
"bookmarklet": "Bookmarklet"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { pushState } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import humanNumber from 'human-number';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
@@ -37,11 +40,11 @@ export function videoLength(lengthSeconds: number): string {
|
||||
|
||||
export interface PhasedDescription {
|
||||
description: string;
|
||||
timestamps: { title: string; time: number; timePretty: string }[];
|
||||
timestamps: { title: string; time: number; timePretty: string; }[];
|
||||
}
|
||||
|
||||
export function phaseDescription(content: string): PhasedDescription {
|
||||
const timestamps: { title: string; time: number; timePretty: string }[] = [];
|
||||
const timestamps: { title: string; time: number; timePretty: string; }[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
const urlRegex = /<a href="([^"]+)"/;
|
||||
@@ -105,3 +108,19 @@ export function proxyVideoUrl(source: string): string {
|
||||
|
||||
return rawSrc.toString();
|
||||
}
|
||||
|
||||
export function unsafeRandomItem(array: any[]): any {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
export function setWindowQueryFlag(key: string, value: string) {
|
||||
const currentPage = get(page);
|
||||
currentPage.url.searchParams.set(key, value);
|
||||
pushState(currentPage.url, currentPage.state);
|
||||
}
|
||||
|
||||
export function removeWindowQueryFlag(key: string) {
|
||||
const currentPage = get(page);
|
||||
currentPage.url.searchParams.delete(key);
|
||||
pushState(currentPage.url, currentPage.state);
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
import Search from '$lib/Search.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { bookmarkletLoadFromUrl, bookmarkletSaveToUrl } from '$lib/bookmarklet';
|
||||
import { removeWindowQueryFlag, setWindowQueryFlag } from '$lib/misc';
|
||||
import type { PlayerEvents } from '$lib/player';
|
||||
import 'beercss';
|
||||
import 'material-dynamic-colors';
|
||||
@@ -109,7 +110,7 @@
|
||||
function changeVideoEvent(conn: DataConnection) {
|
||||
conn.on('data', (data) => {
|
||||
const events = data as PlayerEvents;
|
||||
const currentUrl = new URL(location.href);
|
||||
const currentUrl = get(page).url;
|
||||
|
||||
events.events.forEach((event) => {
|
||||
if (event.type === 'change-video' && event.videoId) {
|
||||
@@ -121,20 +122,15 @@
|
||||
}
|
||||
|
||||
async function startWatchSync() {
|
||||
const currentUrl = new URL(location.href);
|
||||
|
||||
if ($syncPartyPeer) {
|
||||
$syncPartyPeer.destroy();
|
||||
syncPartyPeer.set(null);
|
||||
currentUrl.searchParams.delete('sync');
|
||||
window.history.pushState({ path: currentUrl.toString() }, '', currentUrl.toString());
|
||||
removeWindowQueryFlag('sync');
|
||||
return;
|
||||
}
|
||||
|
||||
const peerId = crypto.randomUUID();
|
||||
currentUrl.searchParams.set('sync', peerId);
|
||||
window.history.pushState({ path: currentUrl.toString() }, '', currentUrl.toString());
|
||||
|
||||
setWindowQueryFlag('sync', peerId);
|
||||
$syncPartyPeer = new Peer(peerId);
|
||||
|
||||
if ($syncPartyPeer) {
|
||||
@@ -248,7 +244,7 @@
|
||||
loadNotifications().catch(() => auth.set(null));
|
||||
}
|
||||
|
||||
const currentUrl = new URL(location.href);
|
||||
const currentUrl = get(page).url;
|
||||
const syncId = currentUrl.searchParams.get('sync');
|
||||
if (syncId) {
|
||||
$syncPartyPeer = new Peer(crypto.randomUUID());
|
||||
@@ -486,7 +482,7 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field no-margin">
|
||||
<div class="field no-margin m l">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.player.theatreModeByDefault')}</div>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { getPlaylist } from '$lib/Api/index';
|
||||
import type { PlaylistPageVideo } from '$lib/Api/model.js';
|
||||
import VideoList from '$lib/VideoList.svelte';
|
||||
import { cleanNumber } from '$lib/misc.js';
|
||||
import { cleanNumber, unsafeRandomItem } from '$lib/misc.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { activePage } from '../../../store';
|
||||
import { activePage, playlistSettings } from '../../../store';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -36,6 +36,31 @@
|
||||
<div class="space"></div>
|
||||
|
||||
<article>
|
||||
{#if videos}
|
||||
<nav>
|
||||
<a
|
||||
href={`/watch/${videos[0].videoId}?playlist=${data.playlist.playlistId}`}
|
||||
class="button circle extra no-margin"
|
||||
>
|
||||
<i>play_arrow</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.playVideos')}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={`/watch/${unsafeRandomItem(videos).videoId}?playlist=${data.playlist.playlistId}`}
|
||||
on:click={() =>
|
||||
playlistSettings.set({ [data.playlist.playlistId]: { shuffle: true, loop: false } })}
|
||||
class="button circle extra no-margin border"
|
||||
>
|
||||
<i>shuffle</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.shuffleVideos')}
|
||||
</div>
|
||||
</a>
|
||||
</nav>
|
||||
{/if}
|
||||
<h3>{data.playlist.title}</h3>
|
||||
<p>
|
||||
{cleanNumber(data.playlist.viewCount)}
|
||||
@@ -43,7 +68,10 @@
|
||||
{$_('videos')}
|
||||
</p>
|
||||
<div class="divider" style="margin-bottom: 1em;"></div>
|
||||
<p style="white-space: pre-line;word-wrap: break-word;">{data.playlist.description}</p>
|
||||
|
||||
<article class="medium scroll no-padding no-elevate no-round">
|
||||
<p style="white-space: pre-line;word-wrap: break-word;">{data.playlist.description}</p>
|
||||
</article>
|
||||
</article>
|
||||
|
||||
{#if videos}
|
||||
|
||||
@@ -12,15 +12,13 @@
|
||||
|
||||
let currentPage = 1;
|
||||
|
||||
$: search = data.search;
|
||||
|
||||
activePage.set(null);
|
||||
|
||||
async function changeType(type: 'playlist' | 'all' | 'video' | 'channel') {
|
||||
data.searchType = type;
|
||||
currentPage = 1;
|
||||
search = [];
|
||||
search = await getSearch(data.slug, { type: type });
|
||||
data.search = [];
|
||||
data.search = await getSearch(data.slug, { type: type });
|
||||
}
|
||||
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
@@ -33,7 +31,7 @@
|
||||
if (newSearch.length === 0) {
|
||||
event.detail.complete();
|
||||
} else {
|
||||
search = [...search, ...newSearch];
|
||||
data.search = [...data.search, ...newSearch];
|
||||
event.detail.loaded();
|
||||
}
|
||||
}
|
||||
@@ -76,11 +74,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if search.length > 0}
|
||||
{#if data.search.length > 0}
|
||||
<div class="page right active">
|
||||
<div class="space"></div>
|
||||
<div class="grid">
|
||||
{#each search as item}
|
||||
{#each data.search as item}
|
||||
<div class="s12 m6 l2">
|
||||
{#key item}
|
||||
<article class="no-padding" style="height: 100%;">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import Comment from '$lib/Comment.svelte';
|
||||
import Player from '$lib/Player.svelte';
|
||||
import Thumbnail from '$lib/Thumbnail.svelte';
|
||||
import { cleanNumber, numberWithCommas } from '$lib/misc.js';
|
||||
import { cleanNumber, numberWithCommas, unsafeRandomItem } from '$lib/misc';
|
||||
import type { PlayerEvents } from '$lib/player';
|
||||
import type { DataConnection } from 'peerjs';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@@ -25,6 +25,7 @@
|
||||
playerAutoplayNextByDefault,
|
||||
playerListenByDefault,
|
||||
playerTheatreModeByDefault,
|
||||
playlistSettings,
|
||||
syncPartyConnections,
|
||||
syncPartyPeer
|
||||
} from '../../../store';
|
||||
@@ -38,6 +39,9 @@
|
||||
let playlistVideos: PlaylistPageVideo[] = [];
|
||||
let playlist: PlaylistPage | null = null;
|
||||
|
||||
let loopPlaylist: boolean = false;
|
||||
let shufflePlaylist: boolean = false;
|
||||
|
||||
let theatreMode = get(playerTheatreModeByDefault);
|
||||
|
||||
let audioMode = get(playerListenByDefault);
|
||||
@@ -45,6 +49,14 @@
|
||||
let seekTo: (time: number) => void;
|
||||
let player: MediaPlayerElement;
|
||||
|
||||
playlistSettings.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 (player) {
|
||||
conn.send({
|
||||
@@ -195,12 +207,49 @@
|
||||
|
||||
if (player) {
|
||||
player.addEventListener('end', () => {
|
||||
if ($playerAutoplayNextByDefault && !playlist) {
|
||||
goto(`/watch/${data.video.recommendedVideos[0].videoId}`);
|
||||
if (!playlistVideos) {
|
||||
if ($playerAutoplayNextByDefault) {
|
||||
goto(`/watch/${data.video.recommendedVideos[0].videoId}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.playlistId) {
|
||||
goToCurrentPlaylistItem();
|
||||
goToCurrentPlaylistItem();
|
||||
|
||||
const playlistVideoIds = playlistVideos.map((value) => {
|
||||
return value.videoId;
|
||||
});
|
||||
|
||||
let goToVideo: PlaylistPageVideo | undefined;
|
||||
|
||||
if (shufflePlaylist) {
|
||||
goToVideo = unsafeRandomItem(playlistVideos);
|
||||
} else {
|
||||
const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId);
|
||||
const newIndex = currentVideoIndex + 1;
|
||||
if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) {
|
||||
goToVideo = playlistVideos[newIndex];
|
||||
} else if (loopPlaylist) {
|
||||
// Loop playlist on end
|
||||
goToVideo = playlistVideos[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof goToVideo !== 'undefined') {
|
||||
if ($syncPartyConnections) {
|
||||
$syncPartyConnections.forEach((conn) => {
|
||||
if (typeof goToVideo === 'undefined') return;
|
||||
|
||||
conn.send({
|
||||
events: [
|
||||
{ type: 'change-video', videoId: goToVideo.videoId },
|
||||
{ type: 'playlist', playlistId: data.playlistId }
|
||||
]
|
||||
} as PlayerEvents);
|
||||
});
|
||||
}
|
||||
|
||||
goto(`/watch/${goToVideo.videoId}?playlist=${data.playlistId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -210,32 +259,6 @@
|
||||
await loadPlaylist(data.playlistId);
|
||||
|
||||
goToCurrentPlaylistItem();
|
||||
|
||||
if (playlistVideos && player) {
|
||||
player.addEventListener('end', () => {
|
||||
if (!playlistVideos) return;
|
||||
|
||||
const playlistVideoIds = playlistVideos.map((value) => {
|
||||
return value.videoId;
|
||||
});
|
||||
|
||||
const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId);
|
||||
const newIndex = currentVideoIndex + 1;
|
||||
if (currentVideoIndex !== -1 && newIndex <= playlistVideoIds.length) {
|
||||
if ($syncPartyConnections) {
|
||||
$syncPartyConnections.forEach((conn) => {
|
||||
conn.send({
|
||||
events: [
|
||||
{ type: 'change-video', videoId: playlistVideos[newIndex].videoId },
|
||||
{ type: 'playlist', playlistId: data.playlistId }
|
||||
]
|
||||
} as PlayerEvents);
|
||||
});
|
||||
}
|
||||
goto(`/watch/${playlistVideos[newIndex].videoId}?playlist=${data.playlistId}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -533,6 +556,40 @@
|
||||
{$_('videos')}
|
||||
</p>
|
||||
<p><a href={`/channel/${playlist.authorId}`}>{playlist.author}</a></p>
|
||||
<nav>
|
||||
<button
|
||||
on:click={() => (
|
||||
(loopPlaylist = !loopPlaylist),
|
||||
playlistSettings.set({
|
||||
[playlist.playlistId]: { loop: loopPlaylist, shuffle: shufflePlaylist }
|
||||
})
|
||||
)}
|
||||
class="circle"
|
||||
class:fill={!loopPlaylist}
|
||||
>
|
||||
<i>loop</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.loopPlaylist')}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
on:click={() => (
|
||||
(shufflePlaylist = !shufflePlaylist),
|
||||
playlistSettings.set({
|
||||
[playlist.playlistId]: { loop: loopPlaylist, shuffle: shufflePlaylist }
|
||||
})
|
||||
)}
|
||||
class="circle"
|
||||
class:fill={!shufflePlaylist}
|
||||
>
|
||||
<i>shuffle</i>
|
||||
<div class="tooltip bottom">
|
||||
{$_('playlist.shuffleVideos')}
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="divider"></div>
|
||||
</article>
|
||||
|
||||
@@ -545,7 +602,9 @@
|
||||
id={playlistVideo.videoId}
|
||||
class:border={playlistVideo.videoId === data.video.videoId}
|
||||
>
|
||||
<Thumbnail video={playlistVideo} playlistId={data.playlistId} />
|
||||
{#key playlistVideo.videoId}
|
||||
<Thumbnail video={playlistVideo} playlistId={data.playlistId || undefined} />
|
||||
{/key}
|
||||
</article>
|
||||
{/each}
|
||||
</article>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const returnYtDislikes = persisted('returnYtDislikes', true);
|
||||
|
||||
export const interfaceSearchSuggestions = persisted('searchSuggestions', true);
|
||||
|
||||
export const auth: Writable<null | { username: string; token: string }> = persisted(
|
||||
export const auth: Writable<null | { username: string; token: string; }> = persisted(
|
||||
'authToken',
|
||||
null
|
||||
);
|
||||
@@ -49,3 +49,5 @@ export const deArrowThumbnailInstance = persisted(
|
||||
|
||||
export const syncPartyPeer: Writable<Peer | null> = writable(null);
|
||||
export const syncPartyConnections: Writable<DataConnection[] | null> = writable();
|
||||
|
||||
export const playlistSettings: Writable<Record<string, { shuffle: boolean, loop: boolean; }>> = writable({});
|
||||
@@ -12,6 +12,12 @@
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
.vds-kb-action {
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
}
|
||||
|
||||
.vds-chapter-radio {
|
||||
align-items: start !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user