From eaf77f7eca844427ceaf4ebd76c8c881c20a1628 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 01:49:56 +1300 Subject: [PATCH 01/14] Bump version --- materialious/android/app/build.gradle | 4 ++-- materialious/electron/materialious.metainfo.xml | 6 +++++- materialious/electron/package.json | 2 +- materialious/package.json | 2 +- update_versions.py | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) 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.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/update_versions.py b/update_versions.py index 92b15e29..84972e4b 100644 --- a/update_versions.py +++ b/update_versions.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime -LATEST_VERSION = "1.15.9" +LATEST_VERSION = "1.16.0" RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious") From 7782e636e5653e9fb8e9f223637ed94d6e10489f Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 03:11:43 +1300 Subject: [PATCH 02/14] Restructure --- materialious/src/lib/api/youtubejs/video.ts | 2 - .../{ => layout}/ContentColumn.svelte | 0 .../components/{ => layout}/ItemsList.svelte | 16 ++++---- .../lib/components/{ => player}/Player.svelte | 14 +++---- .../{ => thumbnail}/ChannelThumbnail.svelte | 6 +-- .../{ => thumbnail}/HashtagThumbnail.svelte | 4 +- .../{ => thumbnail}/PlaylistThumbnail.svelte | 6 +-- .../VideoThumbnail.svelte} | 10 ++--- .../components/{ => watch}/Transcript.svelte | 9 ++-- .../{ => player}/sabr/ShakaPlayerAdapter.ts | 3 +- .../src/lib/{ => player}/sabr/helpers.ts | 0 .../src/lib/{ => player}/sabr/index.ts | 2 - .../thumbnails.ts} | 6 +-- materialious/src/routes/(app)/+layout.svelte | 4 +- materialious/src/routes/(app)/+page.svelte | 2 +- .../routes/(app)/channel/[slug]/+page.svelte | 2 +- .../routes/(app)/hashtag/[slug]/+page.svelte | 2 +- .../routes/(app)/playlist/[slug]/+page.svelte | 2 +- .../routes/(app)/search/[slug]/+page.svelte | 2 +- .../routes/(app)/subscriptions/+page.svelte | 2 +- .../routes/(app)/watch/[slug]/+page.svelte | 4 +- .../(no-layout)/embed/[slug]/+page@.svelte | 2 +- .../routes/(no-layout)/tv/[slug]/+page.svelte | 4 +- .../routes/api/proxy/[urlToProxy]/+server.ts | 41 +++++++++++++++++-- .../static/localApi/ghContributors.json | 2 +- 25 files changed, 90 insertions(+), 57 deletions(-) rename materialious/src/lib/components/{ => layout}/ContentColumn.svelte (100%) rename materialious/src/lib/components/{ => layout}/ItemsList.svelte (94%) rename materialious/src/lib/components/{ => player}/Player.svelte (99%) rename materialious/src/lib/components/{ => thumbnail}/ChannelThumbnail.svelte (88%) rename materialious/src/lib/components/{ => thumbnail}/HashtagThumbnail.svelte (88%) rename materialious/src/lib/components/{ => thumbnail}/PlaylistThumbnail.svelte (91%) rename materialious/src/lib/components/{Thumbnail.svelte => thumbnail/VideoThumbnail.svelte} (97%) rename materialious/src/lib/components/{ => watch}/Transcript.svelte (95%) rename materialious/src/lib/{ => player}/sabr/ShakaPlayerAdapter.ts (99%) rename materialious/src/lib/{ => player}/sabr/helpers.ts (100%) rename materialious/src/lib/{ => player}/sabr/index.ts (98%) rename materialious/src/lib/{timelineThumbnails.ts => player/thumbnails.ts} (96%) 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/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 @@ 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..a3cd4841 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -8,8 +8,8 @@ } 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'; 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..379c0e1c 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 @@ + +{#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..512cc532 --- /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/css/global.css b/materialious/src/lib/css/global.css index 16f69699..b6ffae70 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -133,6 +133,12 @@ menu { border: none !important; } +menu.player-settings { + transform: scale(1) translateY(-40%) translateX(0) !important; + width: 300px !important; + height: 200px !important; +} + @media screen and (max-width: 1000px) { menu.mobile { position: fixed !important; diff --git a/materialious/src/lib/player/android.ts b/materialious/src/lib/player/android.ts new file mode 100644 index 00000000..70587c9f --- /dev/null +++ b/materialious/src/lib/player/android.ts @@ -0,0 +1,95 @@ +import type { VideoPlay } from '$lib/api/model'; +import { playerAndroidLockOrientation, playerAndroidPauseOnNetworkChange } from '$lib/store'; +import { setStatusBarColor } from '$lib/theme'; +import { get } from 'svelte/store'; +import { Capacitor, SystemBars, SystemBarsStyle, SystemBarType } from '@capacitor/core'; +import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; +import { Network, type ConnectionStatus } from '@capacitor/network'; + +export class AndroidPlayer { + video: VideoPlay; + originalOrigination: ScreenOrientationResult | undefined; + initialNetworkStatus: ConnectionStatus | undefined; + playerElement: HTMLMediaElement | undefined; + + constructor(video: VideoPlay, playerElement: HTMLMediaElement | undefined) { + this.video = video; + this.playerElement = playerElement; + + if (Capacitor.getPlatform() !== 'android') return; + + ScreenOrientation.orientation().then((originalOrigination) => { + this.originalOrigination = originalOrigination; + }); + Network.getStatus().then((initialNetworkStatus) => { + this.initialNetworkStatus = initialNetworkStatus; + }); + + document.addEventListener('fullscreenchange', this.onFullscreenChange); + + Network.addListener('networkStatusChange', (networkStatus) => { + if ( + this.initialNetworkStatus?.connectionType !== networkStatus.connectionType && + get(playerAndroidPauseOnNetworkChange) + ) { + this.playerElement?.pause(); + } + }); + } + + async destroy() { + if (Capacitor.getPlatform() !== 'android') return; + + document.removeEventListener('fullscreenchange', this.onFullscreenChange); + + if (this.originalOrigination) { + await ScreenOrientation.lock({ + orientation: this.originalOrigination.type + }); + } + + await Network.removeAllListeners(); + } + + async onFullscreenChange() { + if (Capacitor.getPlatform() !== 'android') return; + + const videoFormats = this.video.adaptiveFormats.filter((format) => + format.type.startsWith('video/') + ); + + const isFullScreen = !!document.fullscreenElement; + + if (isFullScreen) { + // Ensure bar color is black while in fullscreen + await SystemBars.setStyle({ style: SystemBarsStyle.Light }); + await SystemBars.hide({ + bar: SystemBarType.NavigationBar + }); + await SystemBars.hide({ + bar: SystemBarType.StatusBar + }); + } else { + await setStatusBarColor(); + } + + if (!get(playerAndroidLockOrientation)) return; + + if (isFullScreen && videoFormats[0].resolution) { + const widthHeight = videoFormats[0].resolution.split('x'); + + if (widthHeight.length !== 2) return; + + if (Number(widthHeight[0]) > Number(widthHeight[1])) { + await ScreenOrientation.lock({ orientation: 'landscape' }); + } else { + await ScreenOrientation.lock({ orientation: 'portrait' }); + } + } else { + if (this.originalOrigination) + await ScreenOrientation.lock({ + orientation: this.originalOrigination.type + }); + } + } +} diff --git a/materialious/src/lib/player/index.ts b/materialious/src/lib/player/index.ts index de96dc57..4dc41653 100644 --- a/materialious/src/lib/player/index.ts +++ b/materialious/src/lib/player/index.ts @@ -3,6 +3,8 @@ import type { PlaylistPageVideo, VideoPlay } from '$lib/api/model'; import { isAndroidTvStore, playerAutoplayNextByDefaultStore, + playerDefaultLanguage, + playerDefaultQualityStore, playerPlaylistHistory, playlistSettingsStore, syncPartyConnectionsStore @@ -11,6 +13,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'; @@ -104,3 +108,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); + } + } +} From 2f5fe89ec65c876c2617eff2040e155bf60b690b Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 04:50:10 +1300 Subject: [PATCH 04/14] Move timeline to its own component --- .../src/lib/components/player/Player.svelte | 458 ++---------------- .../src/lib/components/player/Timeline.svelte | 378 +++++++++++++++ materialious/src/lib/css/global.css | 43 ++ 3 files changed, 448 insertions(+), 431 deletions(-) create mode 100644 materialious/src/lib/components/player/Timeline.svelte diff --git a/materialious/src/lib/components/player/Player.svelte b/materialious/src/lib/components/player/Player.svelte index afe7d5b3..68afe180 100644 --- a/materialious/src/lib/components/player/Player.svelte +++ b/materialious/src/lib/components/player/Player.svelte @@ -1,8 +1,8 @@ + +
+ {#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/css/global.css b/materialious/src/lib/css/global.css index b6ffae70..045e2bab 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -139,6 +139,49 @@ menu.player-settings { 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; From e0981bcdd7e77825c8233e99b4268390d2bcc033 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 05:01:05 +1300 Subject: [PATCH 05/14] Created TouchControls component --- .../src/lib/components/player/Player.svelte | 118 ++------------- .../components/player/TouchControls.svelte | 135 ++++++++++++++++++ 2 files changed, 146 insertions(+), 107 deletions(-) create mode 100644 materialious/src/lib/components/player/TouchControls.svelte diff --git a/materialious/src/lib/components/player/Player.svelte b/materialious/src/lib/components/player/Player.svelte index 68afe180..1d34f4b0 100644 --- a/materialious/src/lib/components/player/Player.svelte +++ b/materialious/src/lib/components/player/Player.svelte @@ -61,6 +61,7 @@ import FullscreenToggle from './settings/FullscreenToggle.svelte'; import { AndroidPlayer } from '$lib/player/android'; import Timeline from './Timeline.svelte'; + import TouchControls from './TouchControls.svelte'; interface Props { data: { video: VideoPlay; content: ParsedDescription; playlistId: string | null }; @@ -97,7 +98,7 @@ let playerVolume = $state($playerPreferredVolumeStore); let playerVideoEndTimePretty: string = $state(''); let playerIsFullscreen = $state(false); - let playerInitalInteract = true; + let playerInitalInteract = $state(true); const playerVolumeSlider = new Slider({ onValueChange: (volumeToSet) => { @@ -111,11 +112,6 @@ step: 0.01 }); - let clickCount = $state(0); - let clickCounterTimeout: ReturnType; - - let seekDirection: 'forwards' | 'backwards' | undefined = $state(); - playertheatreModeIsActive.subscribe(async () => { await tick(); updateVideoPlayerHeight(); @@ -778,45 +774,6 @@ } } - function onVideoClick(type: 'pause' | 'seekLeft' | 'seekRight') { - seekDirection = undefined; - - if (!playerElement) return; - - if (isMobile() && playerInitalInteract) { - showPlayerUI(); - playerInitalInteract = false; - clickCount = 0; - return; - } - - clickCount++; - - if (clickCounterTimeout) clearTimeout(clickCounterTimeout); - - clickCounterTimeout = setTimeout(() => { - if (clickCount == 1) { - toggleVideoPlaybackStatus(); - } - clickCount = 0; - }, 200); - - if (clickCount < 2) return; - - if (type === 'seekLeft') { - seekDirection = 'backwards'; - playerElement.currentTime = Math.max(0, playerElement.currentTime - playerDoubleTapSeek); - } else if (type === 'pause') { - toggleFullscreen(); - } else { - seekDirection = 'forwards'; - playerElement.currentTime = Math.min( - playerMaxKnownTime, - playerElement.currentTime + playerDoubleTapSeek - ); - } - } - onDestroy(async () => { if (Capacitor.getPlatform() === 'android') { await setStatusBarColor(); @@ -836,7 +793,6 @@ if (watchProgressInterval) clearInterval(watchProgressInterval); if (sabrAdapter) sabrAdapter.dispose(); - if (clickCounterTimeout) clearTimeout(clickCounterTimeout); if (showPlayerUiTimeout) clearTimeout(showPlayerUiTimeout); if (player) { @@ -887,31 +843,15 @@

{/if} -
-
-
onVideoClick('seekLeft')} role="presentation"> - {#if clickCount > 1 && seekDirection === 'backwards'} -
-

-{(clickCount - 1) * playerDoubleTapSeek}

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

+{(clickCount - 1) * playerDoubleTapSeek}

-
- {/if} -
-
-
+ {#if showControls}
+ import { isMobile } from '$lib/misc'; + import { playerDoubleTapSeek } from '$lib/player'; + import { onDestroy } from 'svelte'; + + let { + playerElement, + showPlayerUI, + toggleVideoPlaybackStatus, + toggleFullscreen, + playerMaxKnownTime = $bindable(), + playerIsBuffering = $bindable(), + playerInitalInteract = $bindable() + }: { + showPlayerUI: () => void; + toggleVideoPlaybackStatus: () => void; + toggleFullscreen: () => void; + playerElement: HTMLMediaElement | undefined; + playerMaxKnownTime: number; + playerIsBuffering: boolean; + playerInitalInteract: boolean; + } = $props(); + + let clickCount = $state(0); + let clickCounterTimeout: ReturnType; + + let seekDirection: 'forwards' | 'backwards' | undefined = $state(); + + function onTouchControl(type: 'pause' | 'seekLeft' | 'seekRight') { + seekDirection = undefined; + + if (!playerElement) return; + + if (isMobile() && playerInitalInteract) { + showPlayerUI(); + playerInitalInteract = false; + clickCount = 0; + return; + } + + clickCount++; + + if (clickCounterTimeout) clearTimeout(clickCounterTimeout); + + clickCounterTimeout = setTimeout(() => { + if (clickCount == 1) { + toggleVideoPlaybackStatus(); + } + clickCount = 0; + }, 200); + + if (clickCount < 2) return; + + if (type === 'seekLeft') { + seekDirection = 'backwards'; + playerElement.currentTime = Math.max(0, playerElement.currentTime - playerDoubleTapSeek); + } else if (type === 'pause') { + toggleFullscreen(); + } else { + seekDirection = 'forwards'; + playerElement.currentTime = Math.min( + playerMaxKnownTime, + playerElement.currentTime + playerDoubleTapSeek + ); + } + } + + onDestroy(() => { + if (clickCounterTimeout) clearTimeout(clickCounterTimeout); + }); + + +
+
+
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} +
+
+
+ + From ccd800dbb30005eb7db7c900d33068b9cd95bf50 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 06:07:02 +1300 Subject: [PATCH 06/14] Improved share component --- materialious/src/lib/components/Share.svelte | 85 ++++++++++ .../src/lib/components/ShareVideo.svelte | 79 --------- .../player/settings/CaptionSettings.svelte | 2 +- .../src/lib/components/watch/Playlist.svelte | 122 ++++++++++++++ .../lib/components/watch/Transcript.svelte | 8 +- materialious/src/lib/i18n/locales/en.json | 4 +- materialious/src/lib/misc.ts | 8 + materialious/src/lib/player/index.ts | 5 + .../routes/(app)/channel/[slug]/+page.svelte | 56 +++---- .../routes/(app)/playlist/[slug]/+page.svelte | 59 +++---- .../routes/(app)/watch/[slug]/+page.svelte | 156 +++++------------- 11 files changed, 319 insertions(+), 265 deletions(-) create mode 100644 materialious/src/lib/components/Share.svelte delete mode 100644 materialious/src/lib/components/ShareVideo.svelte create mode 100644 materialious/src/lib/components/watch/Playlist.svelte diff --git a/materialious/src/lib/components/Share.svelte b/materialious/src/lib/components/Share.svelte new file mode 100644 index 00000000..21358f6d --- /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/player/settings/CaptionSettings.svelte b/materialious/src/lib/components/player/settings/CaptionSettings.svelte index 512cc532..fe2408e2 100644 --- a/materialious/src/lib/components/player/settings/CaptionSettings.svelte +++ b/materialious/src/lib/components/player/settings/CaptionSettings.svelte @@ -16,7 +16,7 @@ {#if playerTextTracks && playerTextTracks.length > 0 && !video.liveNow} + + + + + +
    +
    + + +
    + + {#each playlist.videos as playlistVideo, index (index)} +
    + {#key playlistVideo.videoId} + + {/key} +
    + {/each} + diff --git a/materialious/src/lib/components/watch/Transcript.svelte b/materialious/src/lib/components/watch/Transcript.svelte index 12d79c2f..988f600f 100644 --- a/materialious/src/lib/components/watch/Transcript.svelte +++ b/materialious/src/lib/components/watch/Transcript.svelte @@ -86,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/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index ece771ef..aa7730c1 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -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" }, diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 02edcb0c..514afc79 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 { diff --git a/materialious/src/lib/player/index.ts b/materialious/src/lib/player/index.ts index 4dc41653..cbc0d380 100644 --- a/materialious/src/lib/player/index.ts +++ b/materialious/src/lib/player/index.ts @@ -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) { diff --git a/materialious/src/routes/(app)/channel/[slug]/+page.svelte b/materialious/src/routes/(app)/channel/[slug]/+page.svelte index 0a00c71b..fc5b5af1 100644 --- a/materialious/src/routes/(app)/channel/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/channel/[slug]/+page.svelte @@ -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 @@

    {#if !$isAndroidTvStore} - + {/if}
    diff --git a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte index 853e6602..75cb074c 100644 --- a/materialious/src/routes/(app)/playlist/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/playlist/[slug]/+page.svelte @@ -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(); @@ -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)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index a3cd4841..3ad73442 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -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')}
    - + 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)}
    From c4643cef101038e0769b776733688ffd57245910 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 06:07:27 +1300 Subject: [PATCH 07/14] Update Share.svelte --- materialious/src/lib/components/Share.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/materialious/src/lib/components/Share.svelte b/materialious/src/lib/components/Share.svelte index 21358f6d..be2be532 100644 --- a/materialious/src/lib/components/Share.svelte +++ b/materialious/src/lib/components/Share.svelte @@ -3,7 +3,7 @@ import { _ } from '$lib/i18n'; import { invidiousInstanceStore } from '$lib/store'; import { Capacitor } from '@capacitor/core'; - import { titleCase } from '$lib/letterCasing'; + import { sentenceCase } from '$lib/letterCasing'; type ShareLink = { type: 'invidious' | 'youtube' | 'materialious' | 'invidious redirect'; @@ -75,7 +75,7 @@ >
    {$_('player.share.copyXLink', { - linkType: titleCase(share.type) + linkType: sentenceCase(share.type) })}
    From 45895b98f2347ecc75342ca9acd1ea64b8af11ac Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 06:09:20 +1300 Subject: [PATCH 08/14] Import fixes --- materialious/src/routes/(app)/playlists/+page.svelte | 4 ++-- materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/(no-layout)/tv/[slug]/+page.svelte b/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte index 379c0e1c..9f93bb1d 100644 --- a/materialious/src/routes/(no-layout)/tv/[slug]/+page.svelte +++ b/materialious/src/routes/(no-layout)/tv/[slug]/+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/+layout.svelte b/materialious/src/routes/+layout.svelte index dcc0a5b6..e7f81a04 100644 --- a/materialious/src/routes/+layout.svelte +++ b/materialious/src/routes/+layout.svelte @@ -83,11 +83,12 @@ onMount(async () => { ui(); - if (Capacitor.getPlatform() === 'android' && $themeColorStore) { + if (Capacitor.getPlatform() === 'android' && !$themeColorStore) { try { const colorPalette = await colorTheme.getColorPalette(); - themeColorStore.set(convertToHexColorCode(colorPalette.primary)); - await ui('theme', $themeColorStore); + const colorAsHex = convertToHexColorCode(colorPalette.primary); + themeColorStore.set(colorAsHex); + await ui('theme', colorAsHex); } catch { // Continue regardless of error } diff --git a/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts b/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts index c5cb302c..7f676510 100644 --- a/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts +++ b/materialious/src/routes/api/proxy/[urlToProxy]/+server.ts @@ -106,6 +106,11 @@ async function proxyRequest( requestHeaders.set('origin', urlToProxyObj.origin); requestHeaders.set('user-agent', USER_AGENT); + const authHeader = request.headers.get('Authorization'); + if (authHeader) { + requestHeaders.set('Authorization', authHeader); + } + const requestOptions: RequestInit = { method: request.method, headers: requestHeaders, diff --git a/materialious/src/routes/invidious/auth/+page.ts b/materialious/src/routes/invidious/auth/+page.ts new file mode 100644 index 00000000..5fa87762 --- /dev/null +++ b/materialious/src/routes/invidious/auth/+page.ts @@ -0,0 +1,17 @@ +import { resolve } from '$app/paths'; +import { invidiousAuthStore } from '$lib/store'; +import { redirect } from '@sveltejs/kit'; + +export async function load({ url }) { + const username = url.searchParams.get('username'); + const token = url.searchParams.get('token'); + + if (username && token) { + invidiousAuthStore.set({ + username: username, + token: token + }); + } + + throw redirect(302, resolve('/', {})); +} From 6f6885404a40ae65d2b44234c9bc0e243daec7b5 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 07:13:43 +1300 Subject: [PATCH 12/14] Fix to Android color theme --- .../src/lib/components/player/Player.svelte | 90 ++++++++++++++++-- materialious/src/lib/player/android.ts | 95 ------------------- materialious/src/routes/+layout.svelte | 24 ++--- 3 files changed, 95 insertions(+), 114 deletions(-) delete mode 100644 materialious/src/lib/player/android.ts diff --git a/materialious/src/lib/components/player/Player.svelte b/materialious/src/lib/components/player/Player.svelte index 1d34f4b0..339ecfa0 100644 --- a/materialious/src/lib/components/player/Player.svelte +++ b/materialious/src/lib/components/player/Player.svelte @@ -3,7 +3,7 @@ import { getBestThumbnail } from '$lib/images'; import { videoLength } from '$lib/numbers'; import { generateChapterWebVTT, type ParsedDescription } from '$lib/description'; - import { Capacitor } from '@capacitor/core'; + import { Capacitor, SystemBars, SystemBarsStyle, SystemBarType } from '@capacitor/core'; import { error, type Page } from '@sveltejs/kit'; import Mousetrap from 'mousetrap'; import { CapacitorMusicControls } from 'capacitor-music-controls-plugin'; @@ -20,6 +20,8 @@ invidiousInstanceStore, isAndroidTvStore, playerAlwaysLoopStore, + playerAndroidLockOrientation, + playerAndroidPauseOnNetworkChange, playerAutoPlayStore, playerCCByDefault, playerDefaultLanguage, @@ -59,9 +61,10 @@ import Airplay from './settings/Airplay.svelte'; import Pip from './settings/Pip.svelte'; import FullscreenToggle from './settings/FullscreenToggle.svelte'; - import { AndroidPlayer } from '$lib/player/android'; import Timeline from './Timeline.svelte'; import TouchControls from './TouchControls.svelte'; + import { Network, type ConnectionStatus } from '@capacitor/network'; + import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; interface Props { data: { video: VideoPlay; content: ParsedDescription; playlistId: string | null }; @@ -86,11 +89,12 @@ let watchProgressInterval: ReturnType; let showVideoRetry = $state(false); - let androidPlayer: AndroidPlayer; - let player: shaka.Player; let sabrAdapter: SabrStreamingAdapter | null; + let androidInitialNetworkStatus: ConnectionStatus | undefined; + let androidOriginalOrigination: ScreenOrientationResult | undefined; + let playerContainer: HTMLElement; let playerCurrentPlaybackState = $state(false); let playerMaxKnownTime = $state(data.video.lengthSeconds); @@ -369,6 +373,58 @@ }, 5000); } + async function onAndroidFullscreenChange() { + const videoFormats = data.video.adaptiveFormats.filter((format) => + format.type.startsWith('video/') + ); + + const isFullScreen = !!document.fullscreenElement; + + if (isFullScreen) { + // Ensure bar color is black while in fullscreen + await SystemBars.setStyle({ style: SystemBarsStyle.Light }); + await SystemBars.hide({ + bar: SystemBarType.NavigationBar + }); + await SystemBars.hide({ + bar: SystemBarType.StatusBar + }); + } else { + await setStatusBarColor(); + } + + if (!$playerAndroidLockOrientation) return; + + if (isFullScreen && videoFormats[0].resolution) { + const widthHeight = videoFormats[0].resolution.split('x'); + + if (widthHeight.length !== 2) return; + + if (Number(widthHeight[0]) > Number(widthHeight[1])) { + await ScreenOrientation.lock({ orientation: 'landscape' }); + } else { + await ScreenOrientation.lock({ orientation: 'portrait' }); + } + } else { + await ScreenOrientation.lock({ + orientation: (androidOriginalOrigination as ScreenOrientationResult).type + }); + } + } + + async function androidHandleRotate() { + if ( + Capacitor.getPlatform() !== 'android' || + data.video.adaptiveFormats.length === 0 || + $isAndroidTvStore + ) + return; + + androidOriginalOrigination = await ScreenOrientation.orientation(); + + document.addEventListener('fullscreenchange', onAndroidFullscreenChange); + } + onMount(async () => { shaka.polyfill.installAll(); if (!shaka.Player.isBrowserSupported()) { @@ -417,8 +473,6 @@ window.addEventListener('resize', updateVideoPlayerHeight); updateVideoPlayerHeight(); - androidPlayer = new AndroidPlayer(data.video, playerElement); - await player.attach(playerElement); player?.addEventListener('error', (event) => { @@ -454,6 +508,19 @@ playerElement?.addEventListener('volumechange', saveVolumePreference); if (Capacitor.getPlatform() === 'android') { + await androidHandleRotate(); + + androidInitialNetworkStatus = await Network.getStatus(); + + Network.addListener('networkStatusChange', (networkStatus) => { + if ( + androidInitialNetworkStatus?.connectionType !== networkStatus.connectionType && + $playerAndroidPauseOnNetworkChange + ) { + playerElement?.pause(); + } + }); + await CapacitorMusicControls.create({ track: data.video.title, artist: data.video.author, @@ -778,7 +845,16 @@ if (Capacitor.getPlatform() === 'android') { await setStatusBarColor(); await CapacitorMusicControls.destroy(); - await androidPlayer.destroy(); + + if ($isAndroidTvStore) { + document.removeEventListener('fullscreenchange', onAndroidFullscreenChange); + } + + if (androidOriginalOrigination) { + await ScreenOrientation.lock({ + orientation: androidOriginalOrigination.type + }); + } } try { diff --git a/materialious/src/lib/player/android.ts b/materialious/src/lib/player/android.ts deleted file mode 100644 index 70587c9f..00000000 --- a/materialious/src/lib/player/android.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { VideoPlay } from '$lib/api/model'; -import { playerAndroidLockOrientation, playerAndroidPauseOnNetworkChange } from '$lib/store'; -import { setStatusBarColor } from '$lib/theme'; -import { get } from 'svelte/store'; -import { Capacitor, SystemBars, SystemBarsStyle, SystemBarType } from '@capacitor/core'; -import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation'; -import { Network, type ConnectionStatus } from '@capacitor/network'; - -export class AndroidPlayer { - video: VideoPlay; - originalOrigination: ScreenOrientationResult | undefined; - initialNetworkStatus: ConnectionStatus | undefined; - playerElement: HTMLMediaElement | undefined; - - constructor(video: VideoPlay, playerElement: HTMLMediaElement | undefined) { - this.video = video; - this.playerElement = playerElement; - - if (Capacitor.getPlatform() !== 'android') return; - - ScreenOrientation.orientation().then((originalOrigination) => { - this.originalOrigination = originalOrigination; - }); - Network.getStatus().then((initialNetworkStatus) => { - this.initialNetworkStatus = initialNetworkStatus; - }); - - document.addEventListener('fullscreenchange', this.onFullscreenChange); - - Network.addListener('networkStatusChange', (networkStatus) => { - if ( - this.initialNetworkStatus?.connectionType !== networkStatus.connectionType && - get(playerAndroidPauseOnNetworkChange) - ) { - this.playerElement?.pause(); - } - }); - } - - async destroy() { - if (Capacitor.getPlatform() !== 'android') return; - - document.removeEventListener('fullscreenchange', this.onFullscreenChange); - - if (this.originalOrigination) { - await ScreenOrientation.lock({ - orientation: this.originalOrigination.type - }); - } - - await Network.removeAllListeners(); - } - - async onFullscreenChange() { - if (Capacitor.getPlatform() !== 'android') return; - - const videoFormats = this.video.adaptiveFormats.filter((format) => - format.type.startsWith('video/') - ); - - const isFullScreen = !!document.fullscreenElement; - - if (isFullScreen) { - // Ensure bar color is black while in fullscreen - await SystemBars.setStyle({ style: SystemBarsStyle.Light }); - await SystemBars.hide({ - bar: SystemBarType.NavigationBar - }); - await SystemBars.hide({ - bar: SystemBarType.StatusBar - }); - } else { - await setStatusBarColor(); - } - - if (!get(playerAndroidLockOrientation)) return; - - if (isFullScreen && videoFormats[0].resolution) { - const widthHeight = videoFormats[0].resolution.split('x'); - - if (widthHeight.length !== 2) return; - - if (Number(widthHeight[0]) > Number(widthHeight[1])) { - await ScreenOrientation.lock({ orientation: 'landscape' }); - } else { - await ScreenOrientation.lock({ orientation: 'portrait' }); - } - } else { - if (this.originalOrigination) - await ScreenOrientation.lock({ - orientation: this.originalOrigination.type - }); - } - } -} diff --git a/materialious/src/routes/+layout.svelte b/materialious/src/routes/+layout.svelte index e7f81a04..ffe6faec 100644 --- a/materialious/src/routes/+layout.svelte +++ b/materialious/src/routes/+layout.svelte @@ -81,20 +81,20 @@ }); onMount(async () => { - ui(); - - if (Capacitor.getPlatform() === 'android' && !$themeColorStore) { - try { - const colorPalette = await colorTheme.getColorPalette(); - const colorAsHex = convertToHexColorCode(colorPalette.primary); - themeColorStore.set(colorAsHex); - await ui('theme', colorAsHex); - } catch { - // Continue regardless of error - } - } + await ui(); if (Capacitor.getPlatform() === 'android') { + if (!$themeColorStore) { + try { + const colorPalette = await colorTheme.getColorPalette(); + const colorAsHex = convertToHexColorCode(colorPalette.primary); + themeColorStore.set(colorAsHex); + await ui('theme', colorAsHex); + } catch { + // Continue regardless of error + } + } + document.addEventListener('click', async (event: MouseEvent) => { // Handles opening links in browser for android. const link = (event.target as HTMLElement).closest('a'); From f408e76ba18b70953253c91eff4dc103d0aed136 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 07:20:14 +1300 Subject: [PATCH 13/14] Lint fixes --- materialious/src/routes/(app)/channel/[slug]/+page.svelte | 2 -- materialious/src/routes/(app)/watch/[slug]/+page.svelte | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/materialious/src/routes/(app)/channel/[slug]/+page.svelte b/materialious/src/routes/(app)/channel/[slug]/+page.svelte index fc5b5af1..4e4c1e66 100644 --- a/materialious/src/routes/(app)/channel/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/channel/[slug]/+page.svelte @@ -5,8 +5,6 @@ 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/layout/ItemsList.svelte'; diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 3ad73442..093d09dc 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -11,8 +11,7 @@ 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, @@ -23,7 +22,6 @@ playerTheatreModeByDefaultStore, playertheatreModeIsActive, playlistCacheStore, - playlistSettingsStore, syncPartyConnectionsStore, type PlayerState } from '$lib/store'; From 86dbb3c255fc6db938be068e516912be46ebf109 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 18 Feb 2026 07:21:44 +1300 Subject: [PATCH 14/14] Update +page.svelte --- materialious/src/routes/(app)/watch/[slug]/+page.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 093d09dc..0c9ec3f9 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -41,6 +41,7 @@ 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();