Merge pull request #268 from Materialious/update/release-1.0.4

Update/release 1.0.4
This commit is contained in:
Ward
2024-05-09 12:21:03 +12:00
committed by GitHub
16 changed files with 382 additions and 183 deletions
+67
View File
@@ -95,6 +95,8 @@ The following Invidious values must be set in your config.
## Step 3: Docker
Please ensure you have followed the previous steps before doing this!
### Docker Compose
```yaml
version: "3"
services:
@@ -121,6 +123,71 @@ services:
# URL to DeArrow thumbnail instance
VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE: "https://dearrow-thumb.ajay.app"
# Look at "Overwriting Materialious defaults" for all the accepted values.
VITE_DEFAULT_SETTINGS: '{"themeColor": "#2596be"}'
```
### Overwriting Materialious defaults
Materialious allows you to overwrite the default values using `VITE_DEFAULT_SETTINGS`
```json
{
// Set to true to enable dark mode, or false to disable it.
"darkMode": true,
// Specifies the theme color in hexadecimal format (e.g., #ff0000 for red).
"themeColor": "#ff0000",
// Set to true to enable autoplay, or false to disable it.
"autoPlay": false,
// Set to true to always loop videos, or false to loop only when specified.
"alwaysLoop": false,
// Set to true to proxy videos, or false to play them directly.
"proxyVideos": true,
// Set to true to enable listening by default, or false to disable it.
"listenByDefault": true,
// Set to true to save playback position, or false to reset it.
"savePlaybackPosition": true,
// Set to true to enable DASH playback, or false to use other formats.
"dashEnabled": true,
// Set to true to enable theatre mode by default, or false to disable it.
"theatreModeByDefault": false,
// Set to true to autoplay next video by default, or false to disable it.
"autoplayNextByDefault": true,
// Set to true to return YouTube dislikes, or false to hide them.
"returnYtDislikes": false,
// Set to true to enable search suggestions, or false to disable them.
"searchSuggestions": true,
// Set to true to preview video on hover, or false to disable it.
"previewVideoOnHover": true,
// Set to true to enable sponsor block, or false to disable it.
"sponsorBlock": false,
// Specifies the categories for sponsor block as comma-separated values.
// https://wiki.sponsor.ajay.app/w/Types
"sponsorBlockCategories": "sponsor,interaction",
// Set to true to enable deArrow, or false to disable it.
"deArrowEnabled": true,
// Set to true to enable mini player, or false to disable it.
"playerMiniPlayer": true,
// Set to true to enable syncious, or false to disable it.
"syncious": true,
}
```
## Step 4 (Optional, but recommended): Self-host RYD-Proxy
+1
View File
@@ -18,6 +18,7 @@ RUN echo "VITE_DEFAULT_INVIDIOUS_INSTANCE=VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEH
echo "VITE_DEFAULT_PEERJS_PATH=VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER" >> .env && \
echo "VITE_DEFAULT_PEERJS_PORT=VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER" >> .env && \
echo "VITE_DEFAULT_SYNCIOUS_INSTANCE=VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER" >> .env && \
echo "VITE_DEFAULT_SETTINGS=VITE_DEFAULT_SETTINGS_PLACEHOLDER" >> .env && \
echo "VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE=VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE_PLACEHOLDER" >> .env
# Install dependencies and build the project
+1
View File
@@ -11,3 +11,4 @@ find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VI
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PATH_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PATH"'|g' {} +
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_PEERJS_PORT_PLACEHOLDER|'"$VITE_DEFAULT_PEERJS_PORT"'|g' {} +
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SYNCIOUS_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_SYNCIOUS_INSTANCE"'|g' {} +
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SETTINGS_PLACEHOLDER|'"$VITE_DEFAULT_SETTINGS"'|g' {} +
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { bookmarkletSaveToUrl } from '$lib/bookmarklet';
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
import { Capacitor } from '@capacitor/core';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts">
import { Capacitor } from '@capacitor/core';
import { _ } from 'svelte-i18n';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from './Api/model';
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
</script>
{#if !Capacitor.isNativePlatform()}
<a
class="row"
href="#copy"
on:click={async () =>
await navigator.clipboard.writeText(`${location.origin}watch/${video.videoId}`)}
>
<div class="min">{$_('player.share.materialiousLink')}</div></a
>
{/if}
<a
href="#copy"
class="row"
on:click={async () =>
await navigator.clipboard.writeText(`https://redirect.invidious.io/watch?v=${video.videoId}`)}
>
<div class="min">{$_('player.share.invidiousRedirect')}</div></a
><a
class="row"
href="#copy"
on:click={async () =>
await navigator.clipboard.writeText(`https://www.youtube.com/watch?v=${video.videoId}`)}
>
<div class="min">{$_('player.share.youtubeLink')}</div></a
>
+2 -2
View File
@@ -33,7 +33,7 @@
const peerId = crypto.randomUUID();
setWindowQueryFlag('sync', peerId);
$syncPartyPeerStore = peerJs(peerId);
$syncPartyPeerStore = await peerJs(peerId);
if ($syncPartyPeerStore) {
$syncPartyPeerStore.on('connection', (conn) => {
@@ -68,7 +68,7 @@
const currentUrl = get(page).url;
const syncId = currentUrl.searchParams.get('sync');
if (syncId) {
$syncPartyPeerStore = peerJs(crypto.randomUUID());
$syncPartyPeerStore = await peerJs(crypto.randomUUID());
$syncPartyPeerStore.on('open', () => {
if (!$syncPartyPeerStore) return;
+141 -106
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import {
@@ -14,12 +14,16 @@
} from '../store';
import { getDeArrow, getThumbnail, getVideo, getVideoProgress } from './Api';
import type { Notification, PlaylistPageVideo, Video, VideoBase, VideoPlay } from './Api/model';
import ShareVideo from './ShareVideo.svelte';
import { cleanNumber, getBestThumbnail, proxyVideoUrl, videoLength } from './misc';
import type { PlayerEvents } from './player';
const dispatch = createEventDispatcher();
export let video: VideoBase | Video | Notification | PlaylistPageVideo;
export let playlistId: string = '';
let thumbnailHidden: boolean = false;
let showVideoPreview: boolean = false;
let videoPreview: VideoPlay | null = null;
let videoPreviewMuted: boolean = true;
@@ -56,6 +60,15 @@
}
onMount(async () => {
try {
thumbnailHidden = localStorage.getItem(`v_h_${video.videoId}`) === '1';
} catch {}
if (thumbnailHidden) {
dispatch('videoHidden');
return;
}
let imageSrc = getBestThumbnail(video.videoThumbnails);
if (get(deArrowEnabledStore)) {
@@ -180,119 +193,141 @@
showVideoPreview = true;
}
}
function hideVideo() {
try {
localStorage.setItem(`v_h_${video.videoId}`, '1');
dispatch('videoHidden');
thumbnailHidden = true;
} catch {}
}
</script>
<div
style="position: relative;"
on:mouseover={previewVideo}
on:mouseleave={() => (showVideoPreview = false)}
on:focus={() => {}}
role="region"
>
<a
class="wave thumbnail"
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
on:click={syncChangeVideo}
{#if !thumbnailHidden}
<div
style="position: relative;"
on:mouseover={previewVideo}
on:mouseleave={() => (showVideoPreview = false)}
on:focus={() => {}}
role="region"
>
{#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={proxyVideos
? proxyVideoUrl(videoPreview.formatStreams[0].url)
: 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}
</a>
{#if showVideoPreview && videoPreview}
<button
class="no-padding"
style="position: absolute; bottom: 10px; left: 10px; width: 30px; height: 30px;"
on:click={() => {
videoPreviewMuted = !videoPreviewMuted;
}}
<a
class="wave thumbnail"
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
on:click={syncChangeVideo}
>
<i>
{#if videoPreviewMuted}
volume_off
{#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={proxyVideos
? proxyVideoUrl(videoPreview.formatStreams[0].url)
: videoPreview.formatStreams[0].url}
>
</video>
</div>
{:else}
volume_up
<img
class="responsive"
style="max-width: 100%;min-height: 160px;"
src={img.src}
alt="Thumbnail for video"
/>
{/if}
</i>
</button>
{/if}
</div>
<div class="small-padding">
<nav class="no-margin">
<div class="max">
<a
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
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}
</div>
{#if 'publishedText' in video}
<div>
{cleanNumber(video.viewCount)}{video.publishedText}
{: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}
</div>
</nav>
</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}
</div>
<div class="small-padding">
<nav class="no-margin">
<div class="max">
<a
href={watchUrl.toString()}
data-sveltekit-preload-data="off"
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}
</div>
<nav class="no-margin">
{#if 'publishedText' in video}
<div class="max">
{cleanNumber(video.viewCount)}{video.publishedText}
</div>
<button class="transparent circle">
<i>arrow_drop_down</i>
<menu class="left no-wrap">
<a href="#hide" on:click={hideVideo}>
{$_('hideVideo')}
</a>
<div class="divider"></div>
<ShareVideo {video} />
</menu>
</button>
{/if}
</nav>
</div>
</nav>
</div>
{/if}
<canvas id="canvas" style="display: none;"></canvas>
<div id="video-container" style="display: none;"></div>
+37 -6
View File
@@ -1,21 +1,52 @@
<script lang="ts">
import type { PlaylistPageVideo, Video, VideoBase } from './Api/model';
import { _ } from 'svelte-i18n';
import { get } from 'svelte/store';
import { authStore } from '../store';
import { removePlaylistVideo } from './Api';
import type { Notification, PlaylistPageVideo, Video, VideoBase } from './Api/model';
import Thumbnail from './Thumbnail.svelte';
export let videos: VideoBase[] | Video[] | Notification[] | PlaylistPageVideo[] = [];
export let oneItemPerRow: boolean = false;
export let playlistId: string = '';
export let playlistAuthor: string = '';
let hiddenVideos: string[] = [];
let auth = get(authStore);
async function removePlaylistItem(indexId: string, videoId: string) {
if (!playlistId) return;
await removePlaylistVideo(playlistId, indexId);
hiddenVideos = [...hiddenVideos, videoId];
}
</script>
<div class="page right active">
<div class="space"></div>
<div class="grid">
{#each videos as video}
<div class={`s12 m${oneItemPerRow ? '12' : '6'} l${oneItemPerRow ? '12' : '2'}`}>
<article class="no-padding" style="height: 100%;">
<Thumbnail {video} {playlistId} />
</article>
</div>
{#if !hiddenVideos.includes(video.videoId)}
<div class={`s12 m${oneItemPerRow ? '12' : '6'} l${oneItemPerRow ? '12' : '2'}`}>
<article class="no-padding" style="height: 100%;">
<Thumbnail
on:videoHidden={() => (hiddenVideos = [...hiddenVideos, video.videoId])}
{video}
{playlistId}
/>
{#if auth && decodeURIComponent(auth.username) === playlistAuthor && 'indexId' in video}
<div class="right-align" style="margin: 1em .5em;">
<button
on:click={async () => removePlaylistItem(video.indexId, video.videoId)}
class="tertiary circle small"
>
<i>delete</i>
<div class="tooltip">{$_('delete')}</div>
</button>
</div>
{/if}
</article>
</div>
{/if}
{/each}
</div>
</div>
@@ -143,6 +143,39 @@ const persistedStores = [
}
];
function setStores(toSet: Record<string, any>) {
persistedStores.forEach((store) => {
let userOverwritten: boolean = false;
try {
userOverwritten = localStorage.getItem(store.name) !== null;
} catch { }
let paramValue = toSet[store.name];
if (typeof paramValue !== 'undefined' && !userOverwritten) {
let value: any;
if (store.type === 'array') {
value = paramValue.split(',');
} else if (store.type === 'boolean') {
value = paramValue === 'true';
} else {
value = paramValue;
}
store.store.set(value);
}
});
}
export function loadSettingsFromEnv() {
try {
if (typeof import.meta.env.VITE_DEFAULT_SETTINGS !== 'undefined') {
const defaultSettings = JSON.parse(import.meta.env.VITE_DEFAULT_SETTINGS);
setStores(defaultSettings);
}
} catch { }
}
export function bookmarkletSaveToUrl(): string {
const url = new URL(location.origin);
@@ -159,20 +192,11 @@ export function bookmarkletSaveToUrl(): string {
export function bookmarkletLoadFromUrl() {
const currentPage = get(page);
persistedStores.forEach((store) => {
let paramValue = currentPage.url.searchParams.get(store.name);
if (paramValue) {
let value: any;
const toSet: Record<string, string> = {};
if (store.type === 'array') {
value = paramValue.split(',');
} else if (store.type === 'boolean') {
value = paramValue === 'true';
} else {
value = paramValue;
}
store.store.set(value);
}
currentPage.url.searchParams.forEach((value, key) => {
toSet[key] = value;
});
setStores(toSet);
}
+3 -1
View File
@@ -8,9 +8,11 @@
"create": "Create",
"title": "Title",
"searchPlaceholder": "Search...",
"delete": "Delete",
"deleteAllHistory": "Delete all history",
"skipping": "Skipping",
"replies": "replies",
"hideVideo": "Hide video",
"syncParty": {
"userJoined": "User joined your watch party.",
"userLeft": "User left your watch party."
@@ -116,4 +118,4 @@
}
},
"subscribe": "Subscribe"
}
}
+8 -3
View File
@@ -1,7 +1,7 @@
import { pushState } from '$app/navigation';
import { page } from '$app/stores';
import humanNumber from 'human-number';
import { Peer } from 'peerjs';
import type Peer from 'peerjs';
import { get } from 'svelte/store';
import { instanceStore } from '../store';
import type { Image } from './Api/model';
@@ -134,8 +134,13 @@ export function removeWindowQueryFlag(key: string) {
pushState(currentPage.url, currentPage.state);
}
export function peerJs(peerId: string): Peer {
return new Peer(
let PeerInstance: typeof Peer;
export async function peerJs(peerId: string): Promise<Peer> {
// https://github.com/peers/peerjs/issues/819
if (typeof PeerInstance === 'undefined') {
PeerInstance = (await import('peerjs')).Peer;
}
return new PeerInstance(
peerId,
{
host: import.meta.env.VITE_DEFAULT_PEERJS_HOST || '0.peerjs.com',
+12 -7
View File
@@ -10,7 +10,7 @@
import Settings from '$lib/Settings.svelte';
import SyncParty from '$lib/SyncParty.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import { bookmarkletLoadFromUrl } from '$lib/bookmarklet';
import { bookmarkletLoadFromUrl, loadSettingsFromEnv } from '$lib/externalSettings';
import { App } from '@capacitor/app';
import { Browser } from '@capacitor/browser';
import { Capacitor } from '@capacitor/core';
@@ -125,8 +125,6 @@
onMount(async () => {
ui();
bookmarkletLoadFromUrl();
const isDark = get(darkModeStore);
if (isDark === null) {
@@ -150,6 +148,11 @@
await ui('theme', themeHex);
}
loadSettingsFromEnv();
// Should always be loaded after env settings
// So user preferences overwrite instance preferences.
bookmarkletLoadFromUrl();
if (isLoggedIn) {
loadNotifications().catch(() => authStore.set(null));
}
@@ -204,10 +207,12 @@
{#if mobileSearchShow}
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
{:else}
<button data-ui="#sync-party" class="circle large transparent">
<i class:primary-text={$syncPartyPeerStore}>group</i>
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
</button>
{#if !Capacitor.isNativePlatform()}
<button data-ui="#sync-party" class="circle large transparent">
<i class:primary-text={$syncPartyPeerStore}>group</i>
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
</button>
{/if}
{#if isLoggedIn}
<button class="circle large transparent" on:click={() => ui('#dialog-notifications')}
><i>notifications</i>
@@ -5,6 +5,7 @@
import PlaylistThumbnail from '$lib/PlaylistThumbnail.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import { cleanNumber, getBestThumbnail } from '$lib/misc';
import { Capacitor } from '@capacitor/core';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
@@ -113,12 +114,14 @@
<i>share</i>
<span>{$_('player.share.title')}</span>
<menu class="no-wrap">
<a
href="#share"
on:click={async () => {
await navigator.clipboard.writeText(location.href);
}}>{$_('player.share.materialiousLink')}</a
>
{#if !Capacitor.isNativePlatform()}
<a
href="#share"
on:click={async () => {
await navigator.clipboard.writeText(location.href);
}}>{$_('player.share.materialiousLink')}</a
>
{/if}
<a
href="#share"
on:click={async () => {
@@ -3,6 +3,7 @@
import type { PlaylistPageVideo } from '$lib/Api/model.js';
import VideoList from '$lib/VideoList.svelte';
import { cleanNumber, unsafeRandomItem } from '$lib/misc.js';
import { Capacitor } from '@capacitor/core';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { activePageStore, playlistSettingsStore } from '../../../store';
@@ -79,12 +80,14 @@
<i>share</i>
<span>{$_('player.share.title')}</span>
<menu class="no-wrap">
<a
href="#share"
on:click={async () => {
await navigator.clipboard.writeText(location.href);
}}>{$_('player.share.materialiousLink')}</a
>
{#if !Capacitor.isNativePlatform()}
<a
href="#share"
on:click={async () => {
await navigator.clipboard.writeText(location.href);
}}>{$_('player.share.materialiousLink')}</a
>
{/if}
<a
href="#share"
on:click={async () => {
@@ -98,5 +101,5 @@
</article>
{#if videos}
<VideoList {videos} playlistId={data.playlist.playlistId} />
<VideoList {videos} playlistAuthor={data.playlist.author} playlistId={data.playlist.playlistId} />
{/if}
@@ -12,6 +12,7 @@
import type { Comments, PlaylistPage, PlaylistPageVideo } from '$lib/Api/model.js';
import Comment from '$lib/Comment.svelte';
import Player from '$lib/Player.svelte';
import ShareVideo from '$lib/ShareVideo.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import { cleanNumber, getBestThumbnail, numberWithCommas, unsafeRandomItem } from '$lib/misc';
import type { PlayerEvents } from '$lib/player';
@@ -373,6 +374,15 @@
async function toggleTheatreMode() {
theatreMode = !theatreMode;
}
function downloadFile(downloadUrl: string) {
const anchor = document.createElement('a');
anchor.href = downloadUrl;
anchor.download = downloadUrl.split('/').pop() || 'unknown';
anchor.target = '_blank';
anchor.click();
document.body.removeChild(anchor);
}
</script>
<svelte:head>
@@ -467,35 +477,8 @@
{$_('player.share.title')}
</div>
<menu class="no-wrap">
<a
class="row"
href="#copy"
on:click={async () =>
await navigator.clipboard.writeText(
`${location.origin}watch/${data.video.videoId}`
)}
>
<div class="min">{$_('player.share.materialiousLink')}</div></a
><a
href="#copy"
class="row"
on:click={async () =>
await navigator.clipboard.writeText(
`https://redirect.invidious.io/watch?v=${data.video.videoId}`
)}
>
<div class="min">{$_('player.share.invidiousRedirect')}</div></a
><a
class="row"
href="#copy"
on:click={async () =>
await navigator.clipboard.writeText(
`https://www.youtube.com/watch?v=${data.video.videoId}`
)}
>
<div class="min">{$_('player.share.youtubeLink')}</div></a
></menu
></button
<ShareVideo video={data.video} />
</menu></button
>
{#if data.downloadOptions.length > 0}
<button class="border"
@@ -503,7 +486,7 @@
<div class="tooltip">{$_('player.download')}</div>
<menu class="no-wrap">
{#each data.downloadOptions as download}
<a class="row" href={download.url} target="_blank" rel="noopener noreferrer"
<a class="row" href="#download" on:click={() => downloadFile(download.url)}
>{download.title}</a
>
{/each}
+6
View File
@@ -50,3 +50,9 @@ main.root {
width: 100% !important;
}
}
@media screen and (max-width: 650px) {
main.root {
padding-top: 6em !important;
}
}