diff --git a/docs/DOCKER-FULL.md b/docs/DOCKER-FULL.md index 728ff4c6..5fec3196 100644 --- a/docs/DOCKER-FULL.md +++ b/docs/DOCKER-FULL.md @@ -95,7 +95,7 @@ volumes: ``` ### Overwriting Materialious defaults -Materialious allows you to overwrite the default values using `VITE_DEFAULT_SETTINGS`, see [SETTINGS](./SETTINGS.md) for more details. +Materialious lets you customize the default settings by overriding them with `VITE_DEFAULT_SETTINGS`. To configure this easily, go to **Settings** → **Interface** and click "Export to JSON." For more details, check the [SETTINGS](./SETTINGS.md) page. **Please note:** These overwrites only apply on 1st load & won't replace existing configuration stored in browser local storage. diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 9c76dc20..6f8c42d3 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "us.materialio.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 217 - versionName "1.15.9" + versionCode 218 + versionName "1.16.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/materialious/electron/materialious.metainfo.xml b/materialious/electron/materialious.metainfo.xml index de438669..d34ad67f 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -82,7 +82,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.16.0 + + https://github.com/Materialious/Materialious/releases/tag/1.15.9 diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index 14ff9ce2..9e22f55c 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.15.7", + "version": "1.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.15.7", + "version": "1.16.0", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 2f30b3bd..e4806509 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.15.9", + "version": "1.16.0", "description": "Modern material design for YouTube and Invidious.", "author": { "name": "Ward Pearce", diff --git a/materialious/package.json b/materialious/package.json index 223b4c79..ca4f02c7 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.15.9", + "version": "1.16.0", "private": true, "scripts": { "dev": "npm run patch:github && vite dev", diff --git a/materialious/src/lib/api/youtubejs/video.ts b/materialious/src/lib/api/youtubejs/video.ts index 7e638ff9..f9d89d3e 100644 --- a/materialious/src/lib/api/youtubejs/video.ts +++ b/materialious/src/lib/api/youtubejs/video.ts @@ -100,8 +100,6 @@ export async function getVideoYTjs(videoId: string): Promise { video.streaming_data.adaptive_formats = video.streaming_data.adaptive_formats.filter( (format) => format.xtags !== 'CgcKAnZiEgEx' ); - } else { - throw new Error('Video did not provide streaming data.'); } const adaptiveFormats: AdaptiveFormats[] = []; diff --git a/materialious/src/lib/components/Share.svelte b/materialious/src/lib/components/Share.svelte new file mode 100644 index 00000000..be2be532 --- /dev/null +++ b/materialious/src/lib/components/Share.svelte @@ -0,0 +1,85 @@ + + + diff --git a/materialious/src/lib/components/ShareVideo.svelte b/materialious/src/lib/components/ShareVideo.svelte deleted file mode 100644 index d82cefda..00000000 --- a/materialious/src/lib/components/ShareVideo.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - - - {#if currentTime !== undefined} -
  • - -
    {$_('player.share.includeTimestamp')}
    -
  • -
    - {/if} - - - -
    diff --git a/materialious/src/lib/components/ContentColumn.svelte b/materialious/src/lib/components/layout/ContentColumn.svelte similarity index 100% rename from materialious/src/lib/components/ContentColumn.svelte rename to materialious/src/lib/components/layout/ContentColumn.svelte diff --git a/materialious/src/lib/components/ItemsList.svelte b/materialious/src/lib/components/layout/ItemsList.svelte similarity index 94% rename from materialious/src/lib/components/ItemsList.svelte rename to materialious/src/lib/components/layout/ItemsList.svelte index 4d8c99f2..e161549f 100644 --- a/materialious/src/lib/components/ItemsList.svelte +++ b/materialious/src/lib/components/layout/ItemsList.svelte @@ -1,16 +1,16 @@ + +
    + {#snippet timelineTooltip(key: 'thumb' | 'timeline', timeInSeconds: number)} + {#if playerTimelineThumbnails.length > 0} + + {/if} + {#if playerCloestSponsor} +

    + {sponsorSegments[playerCloestSponsor.category]} +

    + {:else if playerCloestTimestamp} +

    + {truncate(playerCloestTimestamp.title, 20)} +

    + {/if} + {videoLength(timeInSeconds)} + {/snippet} +
    + {#if !userManualSeeking && playerShowTimelineThumbnail} +
    + {@render timelineTooltip('timeline', playerTimelineTimeHover)} +
    + {/if} +
    +
    + {#if playerSliderInteracted} +
    + {@render timelineTooltip('thumb', currentTime)} +
    + {/if} +
    +
    +
    + {#each content.timestamps as chapter, index (chapter)} +
    + {/each} + {#if !$sponsorBlockTimelineStore} + {#each segments as segment (segment)} +
    + {/each} + {/if} +
    + + diff --git a/materialious/src/lib/components/player/TouchControls.svelte b/materialious/src/lib/components/player/TouchControls.svelte new file mode 100644 index 00000000..d54316ca --- /dev/null +++ b/materialious/src/lib/components/player/TouchControls.svelte @@ -0,0 +1,135 @@ + + +
    +
    +
    onTouchControl('seekLeft')} role="presentation"> + {#if clickCount > 1 && seekDirection === 'backwards'} +
    +

    -{(clickCount - 1) * playerDoubleTapSeek}

    +
    + {/if} +
    +
    onTouchControl('pause')} role="presentation"> +
    + {#if playerIsBuffering} + + {/if} +
    +
    +
    onTouchControl('seekRight')} role="presentation"> + {#if clickCount > 1 && seekDirection === 'forwards'} +
    +

    +{(clickCount - 1) * playerDoubleTapSeek}

    +
    + {/if} +
    +
    +
    + + diff --git a/materialious/src/lib/components/player/settings/Airplay.svelte b/materialious/src/lib/components/player/settings/Airplay.svelte new file mode 100644 index 00000000..def8e0ea --- /dev/null +++ b/materialious/src/lib/components/player/settings/Airplay.svelte @@ -0,0 +1,29 @@ + + +{#if playerElement && hasWebkitShowPlaybackTargetPicker(playerElement)} + +{/if} diff --git a/materialious/src/lib/components/player/settings/CaptionSettings.svelte b/materialious/src/lib/components/player/settings/CaptionSettings.svelte new file mode 100644 index 00000000..fe2408e2 --- /dev/null +++ b/materialious/src/lib/components/player/settings/CaptionSettings.svelte @@ -0,0 +1,41 @@ + + +{#if playerTextTracks && playerTextTracks.length > 0 && !video.liveNow} + +{/if} diff --git a/materialious/src/lib/components/player/settings/FullscreenToggle.svelte b/materialious/src/lib/components/player/settings/FullscreenToggle.svelte new file mode 100644 index 00000000..d4c65578 --- /dev/null +++ b/materialious/src/lib/components/player/settings/FullscreenToggle.svelte @@ -0,0 +1,16 @@ + + + diff --git a/materialious/src/lib/components/player/settings/Pip.svelte b/materialious/src/lib/components/player/settings/Pip.svelte new file mode 100644 index 00000000..805c628a --- /dev/null +++ b/materialious/src/lib/components/player/settings/Pip.svelte @@ -0,0 +1,14 @@ + + +{#if document.pictureInPictureEnabled} + +{/if} diff --git a/materialious/src/lib/components/player/settings/Settings.svelte b/materialious/src/lib/components/player/settings/Settings.svelte new file mode 100644 index 00000000..98425d7b --- /dev/null +++ b/materialious/src/lib/components/player/settings/Settings.svelte @@ -0,0 +1,186 @@ + + + diff --git a/materialious/src/lib/components/settings/Interface.svelte b/materialious/src/lib/components/settings/Interface.svelte index a248efc9..18887455 100644 --- a/materialious/src/lib/components/settings/Interface.svelte +++ b/materialious/src/lib/components/settings/Interface.svelte @@ -1,5 +1,5 @@ + +
    +
    +
    {playlist.info.title}
    +

    + {cleanNumber(playlist.info.viewCount)} + {$_('views')} • {playlist.info.videoCount} + {$_('videos')} +

    +

    + {#if playlist.info.authorId} + {playlist.info.author} + {:else} + {playlist.info.author} + {/if} +

    + + +
    +
    +
    + +
    + + {#each playlist.videos as playlistVideo, index (index)} +
    + {#key playlistVideo.videoId} + + {/key} +
    + {/each} +
    diff --git a/materialious/src/lib/components/Transcript.svelte b/materialious/src/lib/components/watch/Transcript.svelte similarity index 89% rename from materialious/src/lib/components/Transcript.svelte rename to materialious/src/lib/components/watch/Transcript.svelte index 15becb60..988f600f 100644 --- a/materialious/src/lib/components/Transcript.svelte +++ b/materialious/src/lib/components/watch/Transcript.svelte @@ -3,9 +3,9 @@ import Fuse from 'fuse.js'; import { type VTTCue, parseText, type ParsedCaptionsResult } from 'media-captions'; import { _ } from '$lib/i18n'; - import type { VideoPlay } from '../api/model'; - import { decodeHtmlCharCodes } from '../misc'; - import { invidiousInstanceStore } from '../store'; + import type { VideoPlay } from '$lib/api/model'; + import { decodeHtmlCharCodes } from '$lib/misc'; + import { invidiousInstanceStore } from '$lib/store'; interface Props { video: VideoPlay; @@ -57,8 +57,11 @@ isLoading = true; transcript = null; + const resp = await fetch(urlConstructed); + if (!resp.ok) return; transcript = await parseText(await resp.text(), { strict: false }); + transcriptCues = transcript.cues; isLoading = false; @@ -83,7 +86,7 @@
    {$_('transcript')}
    -
    +
    (playerElement.currentTime = cue.startTime)} - class:secondary-container={currentTime >= cue.startTime && currentTime <= cue.endTime} + class:surface-container-highest={currentTime >= cue.startTime && + currentTime <= cue.endTime} >

    {videoLength(cue.startTime)}

    {decodeHtmlCharCodes(cue.text.replace(/<[^>]+>/g, ''))}

    diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index 16f69699..045e2bab 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -133,6 +133,55 @@ menu { border: none !important; } +menu.player-settings { + transform: scale(1) translateY(-40%) translateX(0) !important; + width: 300px !important; + height: 200px !important; +} + +.player-slider { + height: var(--player-timeline-height); + margin: 0 auto; + border-radius: 0.25rem; +} + +.player-slider.full-width { + width: 100%; +} + +.player-slider.volume { + width: 200px; +} + +.player-slider .track { + background: var(--secondary-container); + height: 100%; + position: relative; + border-radius: 0.25rem; +} + +.player-slider .range { + position: absolute; + background: var(--inverse-primary); + inset: 0; + right: var(--percentage-inv); + border-radius: 0.25rem; + opacity: 0.5; +} + +.player-slider [data-melt-slider-thumb] { + position: absolute; + border-radius: 1rem; + background: var(--primary); + left: var(--percentage); + top: 50%; + width: 5px; + height: 35px; + z-index: 3; + cursor: grab; + transform: translate(-50%, -50%); +} + @media screen and (max-width: 1000px) { menu.mobile { position: fixed !important; diff --git a/materialious/src/lib/externalSettings/index.ts b/materialious/src/lib/externalSettings/index.ts index bb30b7d3..6bb2ec83 100644 --- a/materialious/src/lib/externalSettings/index.ts +++ b/materialious/src/lib/externalSettings/index.ts @@ -26,7 +26,6 @@ export async function syncSettingsToBackend() { let initalLoad = true; store.store.subscribe((value) => { if (!get(rawMasterKeyStore)) return; - if (initalLoad) { initalLoad = false; return; @@ -117,6 +116,19 @@ export function bookmarkletSaveToUrl(): string { return url.toString(); } +export function settingsToJson(): string { + const settings: Record = {}; + + for (const { name, store, excludeFromBookmarklet } of persistedStores) { + const value = get(store); + if (!excludeFromBookmarklet) { + settings[name] = value; + } + } + + return JSON.stringify(settings); +} + export function bookmarkletLoadFromUrl() { const toSet: Record = {}; diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index ece771ef..1d2684bc 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -4,6 +4,7 @@ "popularPageDisabled": "Popular page has been disabled by Admins", "premium": "Premium YouTube content can't be watched on Materialious.", "copyUrl": "Copy URL", + "copy": "Copy JSON", "loadMore": "Load more", "views": "views", "login": "Login", @@ -126,9 +127,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" }, @@ -239,6 +238,7 @@ "playerStatistics": "Show player statistics by default" }, "bookmarklet": "Bookmarklet", + "exportToJson": "Export to JSON", "instanceUrl": "Instance URL", "sponsors": { "sponsor": "Sponsor", diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 02edcb0c..a6aaecd8 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -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 { @@ -306,7 +314,7 @@ export async function goToInvidiousLogin() { path.search = searchParams.toString(); await Browser.open({ url: path.toString() }); } else { - searchParams.set('callback_url', `${location.origin}${resolve('/auth', {})}`); + searchParams.set('callback_url', `${location.origin}${resolve('/invidious/auth', {})}`); path.search = searchParams.toString(); document.location.href = path.toString(); } diff --git a/materialious/src/lib/player/index.ts b/materialious/src/lib/player/index.ts index de96dc57..cbc0d380 100644 --- a/materialious/src/lib/player/index.ts +++ b/materialious/src/lib/player/index.ts @@ -3,7 +3,10 @@ import type { PlaylistPageVideo, VideoPlay } from '$lib/api/model'; import { isAndroidTvStore, playerAutoplayNextByDefaultStore, + playerDefaultLanguage, + playerDefaultQualityStore, playerPlaylistHistory, + playerState, playlistSettingsStore, syncPartyConnectionsStore } from '$lib/store'; @@ -11,6 +14,8 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { loadEntirePlaylist } from '$lib/playlist'; import { unsafeRandomItem } from '$lib/misc'; +import type shaka from 'shaka-player/dist/shaka-player.ui'; +import ISO6391 from 'iso-639-1'; export interface PlayerEvent { type: 'pause' | 'seek' | 'change-video' | 'play' | 'playlist' | 'goto'; @@ -30,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( @@ -43,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) { @@ -104,3 +113,90 @@ export async function goToNextVideo(video: VideoPlay, playlistId: string | null) ); } } + +export function restoreQualityPreference(player: shaka.Player) { + const numericValue = parseInt(get(playerDefaultQualityStore), 10); + + if (isNaN(numericValue)) { + player.configure({ abr: { enabled: true } }); + return; + } + + // Get video-only variant tracks + const tracks = player.getVariantTracks().filter((t) => t.height !== null); + + // Sort by resolution descending + const sortedTracks = tracks.sort((a, b) => (b.height as number) - (a.height as number)); + + // Try exact match + let selectedTrack = sortedTracks.find((t) => t.height === numericValue); + + // Try next best (lower than target) + if (!selectedTrack) { + selectedTrack = sortedTracks.find((t) => (t.height as number) < numericValue); + } + + // Try next higher + if (!selectedTrack) { + selectedTrack = sortedTracks.find((t) => (t.height as number) > numericValue); + } + + if (selectedTrack) { + player.configure({ abr: { enabled: false } }); + player.selectVariantTrack(selectedTrack, true); + } else { + player.configure({ abr: { enabled: true } }); + } +} + +export function restoreDefaultLanguage(player: shaka.Player) { + if (!get(playerDefaultLanguage) || get(playerDefaultLanguage) === 'original') { + const languageAndRole = player.getAudioLanguagesAndRoles().find(({ role }) => role === 'main'); + if (languageAndRole !== undefined) { + player.selectAudioLanguage(languageAndRole.language); + return; + } + } else if (get(playerDefaultLanguage)) { + const audioLanguages = player.getAudioLanguages(); + const langCode = ISO6391.getCode(get(playerDefaultLanguage)); + + for (const audioLanguage of audioLanguages) { + if (audioLanguage.startsWith(langCode)) { + player.selectAudioLanguage(audioLanguage); + break; + } + } + } +} + +export function toggleSubtitles(player: shaka.Player) { + const isVisible = player.isTextTrackVisible(); + if (isVisible) { + player.setTextTrackVisibility(false); + } else { + let langCode: string; + if (get(playerDefaultLanguage) === 'original') { + const languageAndRole = player + .getAudioLanguagesAndRoles() + .find(({ role }) => role === 'main'); + + if (!languageAndRole) { + return; + } + + langCode = languageAndRole.language; + } else { + const defaultLanguage = get(playerDefaultLanguage); + langCode = ISO6391.getCode(defaultLanguage); + } + + const tracks = player.getTextTracks(); + + const subtitleTrack = tracks.find((track) => track.language === langCode); + + if (subtitleTrack) { + player.selectTextTrack(subtitleTrack); + player.setTextTrackVisibility(true); + } + } +} diff --git a/materialious/src/lib/sabr/ShakaPlayerAdapter.ts b/materialious/src/lib/player/sabr/ShakaPlayerAdapter.ts similarity index 99% rename from materialious/src/lib/sabr/ShakaPlayerAdapter.ts rename to materialious/src/lib/player/sabr/ShakaPlayerAdapter.ts index 2946778f..fe13bb10 100644 --- a/materialious/src/lib/sabr/ShakaPlayerAdapter.ts +++ b/materialious/src/lib/player/sabr/ShakaPlayerAdapter.ts @@ -23,7 +23,7 @@ import { createRecoverableError, headersToGenericObject, makeResponse -} from '$lib/sabr/helpers'; +} from '$lib/player/sabr/helpers'; interface ShakaResponseArgs { uri: string; @@ -317,7 +317,6 @@ export class ShakaPlayerAdapter implements SabrPlayerAdapter { } if (result) { - abortController.abort(); return this.createShakaResponse({ uri, request, diff --git a/materialious/src/lib/sabr/helpers.ts b/materialious/src/lib/player/sabr/helpers.ts similarity index 100% rename from materialious/src/lib/sabr/helpers.ts rename to materialious/src/lib/player/sabr/helpers.ts diff --git a/materialious/src/lib/sabr/index.ts b/materialious/src/lib/player/sabr/index.ts similarity index 98% rename from materialious/src/lib/sabr/index.ts rename to materialious/src/lib/player/sabr/index.ts index 95edfb02..690b2b94 100644 --- a/materialious/src/lib/sabr/index.ts +++ b/materialious/src/lib/player/sabr/index.ts @@ -34,8 +34,6 @@ export async function injectSabr( }); sabrAdapter.onReloadPlayerResponse(async (reloadContext) => { - console.log('[SABR]', 'Reloading player response...'); - if (!video.ytjs) return; const reloadedInfo = await video.ytjs.innertube.actions.execute('/player', { diff --git a/materialious/src/lib/timelineThumbnails.ts b/materialious/src/lib/player/thumbnails.ts similarity index 96% rename from materialious/src/lib/timelineThumbnails.ts rename to materialious/src/lib/player/thumbnails.ts index 39f76dd4..fd693304 100644 --- a/materialious/src/lib/timelineThumbnails.ts +++ b/materialious/src/lib/player/thumbnails.ts @@ -1,7 +1,7 @@ -import type { StoryBoard } from './api/model'; -import { ImageCache } from './images'; +import type { StoryBoard } from '$lib/api/model'; +import { ImageCache } from '$lib/images'; import { parseText } from 'media-captions'; -import { findElementForTime } from './misc'; +import { findElementForTime } from '$lib/misc'; export interface TimelineThumbnail { url: string; diff --git a/materialious/src/routes/(app)/+layout.svelte b/materialious/src/routes/(app)/+layout.svelte index aa07b8f9..de427e10 100644 --- a/materialious/src/routes/(app)/+layout.svelte +++ b/materialious/src/routes/(app)/+layout.svelte @@ -10,8 +10,8 @@ import Search from '$lib/components/Search.svelte'; import Settings from '$lib/components/settings/Settings.svelte'; import SyncParty from '$lib/components/SyncParty.svelte'; - import Thumbnail from '$lib/components/Thumbnail.svelte'; - import Player from '$lib/components/Player.svelte'; + import Thumbnail from '$lib/components/thumbnail/VideoThumbnail.svelte'; + import Player from '$lib/components/player/Player.svelte'; import '$lib/css/global.css'; import { getPages } from '$lib/navPages'; import { diff --git a/materialious/src/routes/(app)/+page.svelte b/materialious/src/routes/(app)/+page.svelte index 364bf757..59e6a030 100644 --- a/materialious/src/routes/(app)/+page.svelte +++ b/materialious/src/routes/(app)/+page.svelte @@ -1,5 +1,5 @@ - - diff --git a/materialious/src/routes/(app)/auth/+page.ts b/materialious/src/routes/(app)/auth/+page.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/materialious/src/routes/(app)/channel/[slug]/+page.svelte b/materialious/src/routes/(app)/channel/[slug]/+page.svelte index af43b84d..4e4c1e66 100644 --- a/materialious/src/routes/(app)/channel/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/channel/[slug]/+page.svelte @@ -5,15 +5,15 @@ import { proxyGoogleImage } from '$lib/images'; import { cleanNumber } from '$lib/numbers'; import { channelCacheStore, interfaceLowBandwidthMode, isAndroidTvStore } from '$lib/store'; - import { Clipboard } from '@capacitor/clipboard'; - import { Capacitor } from '@capacitor/core'; import { _ } from '$lib/i18n'; import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading'; - import ItemsList from '$lib/components/ItemsList.svelte'; + import ItemsList from '$lib/components/layout/ItemsList.svelte'; import Author from '$lib/components/Author.svelte'; 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 +108,29 @@

    {#if !$isAndroidTvStore} - + {/if}
    diff --git a/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte b/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte index d21b4481..1869d829 100644 --- a/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/hashtag/[slug]/+page.svelte @@ -1,7 +1,7 @@ @@ -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" > shuffle
    @@ -68,36 +68,29 @@
    {#if !$isAndroidTvStore} - + {/if}
    diff --git a/materialious/src/routes/(app)/playlists/+page.svelte b/materialious/src/routes/(app)/playlists/+page.svelte index 6a101ac9..8c76ce82 100644 --- a/materialious/src/routes/(app)/playlists/+page.svelte +++ b/materialious/src/routes/(app)/playlists/+page.svelte @@ -2,8 +2,8 @@ import { preventDefault } from 'svelte/legacy'; import { deletePersonalPlaylist, getPersonalPlaylists, postPersonalPlaylist } from '$lib/api'; - import ContentColumn from '$lib/components/ContentColumn.svelte'; - import PlaylistThumbnail from '$lib/components/PlaylistThumbnail.svelte'; + import ContentColumn from '$lib/components/layout/ContentColumn.svelte'; + import PlaylistThumbnail from '$lib/components/thumbnail/PlaylistThumbnail.svelte'; import { ui } from 'beercss'; import { _ } from '$lib/i18n'; diff --git a/materialious/src/routes/(app)/search/[slug]/+page.svelte b/materialious/src/routes/(app)/search/[slug]/+page.svelte index 86086d34..1907e015 100644 --- a/materialious/src/routes/(app)/search/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/search/[slug]/+page.svelte @@ -4,7 +4,7 @@ import { searchCacheStore } from '$lib/store'; import { _ } from '$lib/i18n'; import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading'; - import ItemsList from '$lib/components/ItemsList.svelte'; + import ItemsList from '$lib/components/layout/ItemsList.svelte'; import type { SearchOptions, SearchResults } from '$lib/api/model'; let { data } = $props(); diff --git a/materialious/src/routes/(app)/subscriptions/+page.svelte b/materialious/src/routes/(app)/subscriptions/+page.svelte index 48de51ca..0576b495 100644 --- a/materialious/src/routes/(app)/subscriptions/+page.svelte +++ b/materialious/src/routes/(app)/subscriptions/+page.svelte @@ -3,7 +3,7 @@ import type { PlaylistPageVideo, Video, VideoBase } from '$lib/api/model'; import { feedCacheStore, feedLoadingStore } from '$lib/store'; import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading'; - import ItemsList from '$lib/components/ItemsList.svelte'; + import ItemsList from '$lib/components/layout/ItemsList.svelte'; import { resolve } from '$app/paths'; import { _ } from '$lib/i18n'; import PageLoading from '$lib/components/PageLoading.svelte'; diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 24bddf7a..0c9ec3f9 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -7,13 +7,11 @@ 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.svelte'; - import Transcript from '$lib/components/Transcript.svelte'; + import Thumbnail from '$lib/components/thumbnail/VideoThumbnail.svelte'; + import Transcript from '$lib/components/watch/Transcript.svelte'; import { getBestThumbnail } from '$lib/images'; import { letterCase } from '$lib/letterCasing'; - import { cleanNumber, numberWithCommas } from '$lib/numbers'; - import { goToNextVideo, goToPreviousVideo, type PlayerEvents } from '$lib/player/index'; + import { numberWithCommas } from '$lib/numbers'; import { invidiousAuthStore, interfaceAutoExpandChapters, @@ -24,7 +22,6 @@ playerTheatreModeByDefaultStore, playertheatreModeIsActive, playlistCacheStore, - playlistSettingsStore, syncPartyConnectionsStore, type PlayerState } from '$lib/store'; @@ -42,6 +39,9 @@ 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'; + import type { PlayerEvents } from '$lib/player/index.js'; let { data = $bindable() } = $props(); @@ -55,9 +55,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 +81,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 +438,44 @@ {$_('transcript')} - + 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} - - - - - -
    -
    -
    - -
    - - {#each $playlistCacheStore[data.playlistId].videos as playlistVideo (playlistVideo.videoId)} -
    - {#key playlistVideo.videoId} - - {/key} -
    - {/each} - + {:else if data.video.recommendedVideos} {#each data.video.recommendedVideos as recommendedVideo (recommendedVideo.videoId)}
    diff --git a/materialious/src/routes/(no-layout)/embed/[slug]/+page@.svelte b/materialious/src/routes/(no-layout)/embed/[slug]/+page@.svelte index 9c6d2b15..d441a8c0 100644 --- a/materialious/src/routes/(no-layout)/embed/[slug]/+page@.svelte +++ b/materialious/src/routes/(no-layout)/embed/[slug]/+page@.svelte @@ -1,5 +1,5 @@ diff --git a/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte b/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte index 5fac02d1..9f93bb1d 100644 --- a/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte +++ b/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte @@ -1,6 +1,6 @@