Merge pull request #129 from WardPearce/update/ui-improvements

Update/UI improvements
This commit is contained in:
Ward
2024-04-13 20:12:01 +12:00
committed by GitHub
8 changed files with 290 additions and 145 deletions
+22 -21
View File
@@ -21,13 +21,13 @@
import { getDynamicTheme } from './theme';
export let data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
export let currentTime: number = 0;
export let audioMode = false;
export let player: MediaPlayerElement;
export let isSyncing: boolean = false;
let src: PlayerSrc = [];
let categoryBeingSkipped = '';
let playerIsLive = false;
export function seekTo(time: number) {
if (typeof player !== 'undefined') {
@@ -38,14 +38,8 @@
const proxyVideos = get(playerProxyVideos);
onMount(async () => {
try {
const defaultVolume = localStorage.getItem('volume');
if (defaultVolume) {
player.volume = Number(defaultVolume);
}
} catch {}
if (!data.video.hlsUrl) {
playerIsLive = false;
if (data.video.captions) {
data.video.captions.forEach(async (caption) => {
player.textTracks.add({
@@ -81,10 +75,6 @@
});
}
player.addEventListener('time-update', () => {
currentTime = player.currentTime;
});
player.addEventListener('pause', () => {
savePlayerPos();
});
@@ -121,6 +111,10 @@
}
if (get(playerDash)) {
player.addEventListener('dash-can-play', () => {
loadPlayerPos();
});
src = [{ src: data.video.dashUrl + '?local=true', type: 'application/dash+xml' }];
} else {
let formattedSrc;
@@ -138,17 +132,11 @@
width: Number(quality[0])
};
});
}
if (get(playerSavePlaybackPosition)) {
try {
const playerPos = localStorage.getItem(`v_${data.video.videoId}`);
if (playerPos) {
player.currentTime = Number(playerPos);
}
} catch {}
loadPlayerPos();
}
} else {
playerIsLive = true;
src = [
{
src: data.video.hlsUrl + '?local=true',
@@ -189,7 +177,20 @@
document.documentElement.style.setProperty('--audio-bg', currentTheme['--surface']);
});
function loadPlayerPos() {
if (get(playerSavePlaybackPosition)) {
try {
const playerPos = localStorage.getItem(`v_${data.video.videoId}`);
if (playerPos) {
player.currentTime = Number(playerPos);
}
} catch {}
}
}
function savePlayerPos() {
if (data.video.hlsUrl) return;
try {
if (get(playerSavePlaybackPosition) && player.currentTime) {
if (player.currentTime < player.duration - 10 && player.currentTime > 10) {
@@ -216,7 +217,7 @@
autoPlay={$playerAutoPlay && !isSyncing}
loop={$playerAlwaysLoop}
title={data.video.title}
streamType={data.video.hlsUrl ? 'live' : 'on-demand'}
streamType={playerIsLive ? 'live' : 'on-demand'}
viewType={audioMode ? 'audio' : 'video'}
keep-alive
{src}
+48 -35
View File
@@ -1,9 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { createEventDispatcher } from 'svelte';
import { _ } from 'svelte-i18n';
import { interfaceSearchSuggestions } from '../store';
import { getSearchSuggestions } from './Api';
const dispatch = createEventDispatcher();
let searchSuggestions = false;
interfaceSearchSuggestions.subscribe((value) => (searchSuggestions = value));
@@ -11,6 +14,8 @@
let suggestionsForSearch: string[] = [];
let selectedSuggestionIndex: number = -1;
let showSearchBox = false;
let debounceTimer: NodeJS.Timeout;
function debouncedSearch(event: any) {
if (!searchSuggestions) return;
@@ -28,6 +33,7 @@
function handleSubmit() {
selectedSuggestionIndex = -1;
goto(`/search/${encodeURIComponent(search)}`);
dispatch('searchSubmitted');
}
function handleKeyDown(event: KeyboardEvent) {
@@ -50,43 +56,50 @@
<form on:submit|preventDefault={handleSubmit}>
<div class="field prefix round fill no-margin search">
<i class="front">search</i>
<input bind:value={search} on:click={() => document.getElementById('search')?.focus()} />
<menu class="min">
<div class="field large prefix suffix no-margin fixed">
<i class="front">arrow_back</i>
<input
placeholder={$_('searchPlaceholder')}
type="text"
id="search"
required
bind:value={search}
on:keydown={handleKeyDown}
on:keyup={(event) => {
if (event.key === 'Enter') {
handleSubmit();
} else {
debouncedSearch(event);
}
}}
/>
<i class="front" on:click={() => (search = '', suggestionsForSearch = [], selectedSuggestionIndex = -1)}>close</i>
</div>
{#if searchSuggestions}
{#each suggestionsForSearch as suggestion, index}
<a
on:click={() => {
search = suggestion;
handleSubmit();
<input bind:value={search} on:click={() => (showSearchBox = true)} />
{#if showSearchBox}
<menu class="min">
<div class="field large prefix suffix no-margin fixed">
<i class="front" on:click={() => dispatch('searchCancelled')}>arrow_back</i>
<input
placeholder={$_('searchPlaceholder')}
type="text"
autofocus={true}
required
bind:value={search}
on:keydown={handleKeyDown}
on:keyup={(event) => {
if (event.key === 'Enter') {
handleSubmit();
} else {
debouncedSearch(event);
}
}}
class="row"
class:selected={index === selectedSuggestionIndex}
href={`/search/${encodeURIComponent(suggestion)}`}
/>
<i
class="front"
on:click={() => (
(search = ''), (suggestionsForSearch = []), (selectedSuggestionIndex = -1)
)}>close</i
>
<div>{suggestion}</div>
</a>
{/each}
{/if}
</menu>
</div>
{#if searchSuggestions}
{#each suggestionsForSearch as suggestion, index}
<a
on:click={() => {
search = suggestion;
handleSubmit();
}}
class="row"
class:selected={index === selectedSuggestionIndex}
href={`/search/${encodeURIComponent(suggestion)}`}
>
<div>{suggestion}</div>
</a>
{/each}
{/if}
</menu>
{/if}
</div>
</form>
+134 -41
View File
@@ -4,18 +4,25 @@
import { get } from 'svelte/store';
import {
deArrowEnabled,
interfacePreviewVideoOnHover,
playerProxyVideos,
playerSavePlaybackPosition,
syncPartyConnections,
syncPartyPeer
} from '../store';
import { getDeArrow, getThumbnail, getVideo } from './Api';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from './Api/model';
import { cleanNumber, proxyVideoUrl, truncate, videoLength } from './misc';
import type { Notification, PlaylistPageVideo, Video, VideoBase, VideoPlay } from './Api/model';
import { cleanNumber, proxyVideoUrl, videoLength } from './misc';
import type { PlayerEvents } from './player';
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
export let playlistId: string = '';
let showVideoPreview: boolean = false;
let videoPreview: VideoPlay | null = null;
let videoPreviewMuted: boolean = true;
let videoPreviewVolume: number = 0.4;
let watchUrl = new URL(`${import.meta.env.VITE_DEFAULT_FRONTEND_URL}/watch/${video.videoId}`);
if (playlistId !== '') {
@@ -30,7 +37,6 @@
let loading = true;
let loaded = false;
let failed = false;
let img: HTMLImageElement;
@@ -118,7 +124,6 @@
};
img.onerror = () => {
loading = false;
failed = true;
};
});
@@ -139,52 +144,126 @@
});
}
}
async function previewVideo() {
if (!get(interfacePreviewVideoOnHover)) return;
showVideoPreview = true;
try {
videoPreview = await getVideo(video.videoId, get(playerProxyVideos));
if (videoPreview.hlsUrl) {
showVideoPreview = false;
videoPreview = null;
} else {
try {
const playerSettings = localStorage.getItem('video-player');
if (playerSettings && typeof playerSettings === 'object' && 'volume' in playerSettings) {
videoPreviewVolume = Number(playerSettings['volume']);
}
} catch {}
}
} catch {
showVideoPreview = true;
}
}
</script>
<a
class="wave"
style="width: 100%; overflow: hidden;min-height:100px;"
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
on:click={syncChangeVideo}
<div
style="position: relative;"
on:mouseover={previewVideo}
on:mouseleave={() => (showVideoPreview = false)}
on:focus={() => {}}
role="region"
>
{#if loading}
<progress class="circle"></progress>
{:else if loaded}
<img
class="responsive"
style="max-width: 100%;min-height: 160px;"
src={img.src}
alt="Thumbnail for video"
/>
{:else}
<p>{$_('thumbnail.failedToLoadImage')}</p>
{/if}
{#if progress}
<progress
class="absolute right bottom"
style="z-index: 1;"
value={progress}
max={video.lengthSeconds}
></progress>
{/if}
{#if !('liveVideo' in video) || !video.liveVideo}
{#if video.lengthSeconds !== 0}
<div class="absolute right bottom small-margin black white-text small-text thumbnail-corner">
&nbsp;{videoLength(video.lengthSeconds)}&nbsp;
<a
class="wave thumbnail"
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
on:click={syncChangeVideo}
>
{#if loading}
<progress class="circle"></progress>
{:else if loaded}
{#if showVideoPreview && videoPreview}
<div style="max-width: 100%; max-height: 200px;">
<video
style="max-width: 100%; max-height: 200px;"
autoplay
poster={img.src}
width="100%"
height="100%"
muted={videoPreviewMuted}
controls={false}
volume={videoPreviewVolume}
src={videoPreview.formatStreams[0].url}
>
</video>
</div>
{:else}
<img
class="responsive"
style="max-width: 100%;min-height: 160px;"
src={img.src}
alt="Thumbnail for video"
/>
{/if}
{:else}
<p>{$_('thumbnail.failedToLoadImage')}</p>
{/if}
{#if progress}
<progress
class="absolute right bottom"
style="z-index: 1;"
value={progress}
max={video.lengthSeconds}
></progress>
{/if}
{#if !('liveVideo' in video) || !video.liveVideo}
{#if video.lengthSeconds !== 0}
<div
class="absolute right bottom small-margin black white-text small-text thumbnail-corner"
>
&nbsp;{videoLength(video.lengthSeconds)}&nbsp;
</div>
{/if}
{:else}
<div class="absolute right bottom small-margin red white-text small-text thumbnail-corner">
{$_('thumbnail.live')}
</div>
{/if}
{:else}
<div class="absolute right bottom small-margin red white-text small-text thumbnail-corner">
{$_('thumbnail.live')}
</div>
</a>
{#if showVideoPreview && videoPreview}
<button
class="no-padding"
style="position: absolute; bottom: 10px; left: 10px; width: 30px; height: 30px;"
on:click={() => {
videoPreviewMuted = !videoPreviewMuted;
}}
>
<i>
{#if videoPreviewMuted}
volume_off
{:else}
volume_up
{/if}
</i>
</button>
{/if}
</a>
</div>
<div class="small-padding">
<nav class="no-margin">
<div class="max">
<a href={watchUrl.toString()}><div class="bold">{truncate(video.title)}</div></a>
<div>
<a
href={watchUrl.toString()}
style="display: flex; justify-content:flex-start; position: absolute; width: 100%;"
><div class="bold" style="white-space: nowrap; overflow: hidden;text-overflow: ellipsis;">
{video.title}
</div>
<div class="tooltip bottom small">{video.title}</div>
</a>
<div style="margin-top: 1.4em;">
<a href={`/channel/${video.authorId}`}>{video.author}</a
>{#if !('publishedText' in video) && 'viewCountText' in video}
&nbsp;{video.viewCountText}{/if}
@@ -200,3 +279,17 @@
<canvas id="canvas" style="display: none;"></canvas>
<div id="video-container" style="display: none;"></div>
<style>
.thumbnail {
width: 100%;
overflow: hidden;
max-height: 160px;
}
@media screen and (max-width: 650px) {
.thumbnail {
max-height: 100%;
}
}
</style>
+6
View File
@@ -5,6 +5,7 @@ import {
deArrowEnabled,
deArrowInstance,
deArrowThumbnailInstance,
interfacePreviewVideoOnHover,
interfaceSearchSuggestions,
playerAlwaysLoop,
playerAutoPlay,
@@ -87,6 +88,11 @@ const persistedStores = [
store: interfaceSearchSuggestions,
type: 'boolean'
},
{
name: 'previewVideoOnHover',
store: interfacePreviewVideoOnHover,
type: 'boolean'
},
{
name: 'sponsorBlock',
store: sponsorBlock,
@@ -79,6 +79,7 @@
"color": "Color"
},
"searchSuggestions": "Search suggestions",
"previewVideoOnHover": "Preview video on hover",
"dataPreferences": {
"dataPreferences": "Data preferences",
"content": "Looking to import/export subscriptions, change password or delete account? Click here and scroll to the bottom of the page."
+77 -39
View File
@@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { navigating, page } from '$app/stores';
import { getFeed } from '$lib/Api/index';
import type { Notification } from '$lib/Api/model';
import Logo from '$lib/Logo.svelte';
import PageLoading from '$lib/PageLoading.svelte';
import Search from '$lib/Search.svelte';
@@ -24,6 +25,7 @@
deArrowEnabled,
deArrowInstance,
deArrowThumbnailInstance,
interfacePreviewVideoOnHover,
interfaceSearchSuggestions,
playerAlwaysLoop,
playerAutoPlay,
@@ -43,12 +45,11 @@
themeColor
} from '../store';
let mobileSearchShow = false;
let currentPage: string | null = '';
activePage.subscribe((page) => (currentPage = page));
let searchSuggestions = false;
interfaceSearchSuggestions.subscribe((value) => (searchSuggestions = value));
let sponsorCategoriesList: string[] = [];
sponsorBlockCategories.subscribe((value) => (sponsorCategoriesList = value));
@@ -291,57 +292,75 @@
</nav>
<nav class="top">
<button class="circle large transparent s m l small-margin" data-ui="#menu-expanded"
{#if !mobileSearchShow}
<button
on:click={() => (mobileSearchShow = !mobileSearchShow)}
class="transparent s circle large"
>
<i>search</i>
</button>
{/if}
<button class="circle large transparent m l small-margin" data-ui="#menu-expanded"
><i>menu</i></button
>
<Logo classes="m l" />
<h6 class="m l">Materialious</h6>
<div class="max"></div>
<div class="max m l"></div>
<div class="m l">
<Search />
</div>
<div class="max"></div>
<a
href="https://github.com/WardPearce/Materialious"
target="_blank"
rel="noopener noreferrer"
class="button circle large transparent"
>
<i>code</i>
<div class="tooltip bottom">{$_('layout.star')}</div>
</a>
<button data-ui="#sync-party" class="circle large transparent">
<i class:primary-text={$syncPartyPeer}>group</i>
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
</button>
{#if isLoggedIn}
<button class="circle large transparent" data-ui="#dialog-notifications"
><i>notifications</i>
<div class="tooltip bottom">{$_('layout.notifications')}</div>
</button>
{#if !mobileSearchShow}
<div class="max"></div>
{/if}
<button class="circle large transparent" data-ui="#dialog-settings">
<i>settings</i>
<div class="tooltip bottom">{$_('layout.settings')}</div>
</button>
{#if !isLoggedIn}
<button on:click={login} class="circle large transparent">
<i>login</i>
<div class="tooltip bottom">{$_('layout.login')}</div>
</button>
{#if mobileSearchShow}
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
{:else}
<button on:click={logout} class="circle large transparent">
<i>logout</i>
<div class="tooltip bottom">{$_('layout.logout')}</div>
<button data-ui="#sync-party" class="m l circle large transparent">
<i class:primary-text={$syncPartyPeer}>group</i>
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
</button>
{#if isLoggedIn}
<button class="circle large transparent" data-ui="#dialog-notifications"
><i>notifications</i>
<div class="tooltip bottom">{$_('layout.notifications')}</div>
</button>
{/if}
<button class="circle large transparent" data-ui="#dialog-settings">
<i>settings</i>
<div class="tooltip bottom">{$_('layout.settings')}</div>
</button>
{#if !isLoggedIn}
<button on:click={login} class="circle large transparent">
<i>login</i>
<div class="tooltip bottom">{$_('layout.login')}</div>
</button>
{:else}
<button on:click={logout} class="circle large transparent">
<i>logout</i>
<div class="tooltip bottom">{$_('layout.logout')}</div>
</button>
{/if}
{/if}
</nav>
<nav class="bottom s">
{#each pages as page}
{#if !page.requiresAuth || isLoggedIn}
<a class="round" href={page.href} class:active={currentPage === page.name.toLowerCase()}
><i>{page.icon}</i>
<div class="tooltip top">{page.name}</div>
</a>
{/if}
{/each}
</nav>
<dialog class="right" id="dialog-settings">
<nav>
<h4 class="max">{$_('layout.settings')}</h4>
@@ -384,6 +403,22 @@
</label>
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.previewVideoOnHover')}</div>
</div>
<label class="switch">
<input
type="checkbox"
bind:checked={$interfacePreviewVideoOnHover}
on:click={() => interfacePreviewVideoOnHover.set(!$interfacePreviewVideoOnHover)}
/>
<span></span>
</label>
</nav>
</div>
</div>
<div class="settings">
@@ -689,9 +724,6 @@
</nav>
</header>
<div class="s">
<Search />
</div>
<div class="space"></div>
{#each pages as page}
{#if !page.requiresAuth || isLoggedIn}
@@ -766,4 +798,10 @@
form {
margin: 1em 0;
}
@media screen and (max-width: 650px) {
dialog.right {
width: 100%;
}
}
</style>
@@ -46,7 +46,6 @@
let theatreMode = get(playerTheatreModeByDefault);
let audioMode = get(playerListenByDefault);
let currentTime: number;
let seekTo: (time: number) => void;
let player: MediaPlayerElement;
@@ -357,14 +356,7 @@
<div class="grid">
<div class={`s12 m12 l${theatreMode ? '12' : '10'}`}>
{#key data.video.videoId}
<Player
{data}
{audioMode}
isSyncing={$syncPartyPeer !== null}
bind:seekTo
bind:currentTime
bind:player
/>
<Player {data} {audioMode} isSyncing={$syncPartyPeer !== null} bind:seekTo bind:player />
{/key}
<h5>{data.video.title}</h5>
+1
View File
@@ -24,6 +24,7 @@ export const playerAutoplayNextByDefault = persisted('autoplayNextByDefault', fa
export const returnYtDislikes = persisted('returnYtDislikes', true);
export const interfaceSearchSuggestions = persisted('searchSuggestions', true);
export const interfacePreviewVideoOnHover = persisted('previewVideoOnHover', true);
export const auth: Writable<null | { username: string; token: string; }> = persisted(
'authToken',