From 83465b45f0d87aacc4d56687026049f576d4703d Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 4 Nov 2025 20:47:23 +1300 Subject: [PATCH 1/8] Bump version --- materialious/android/app/build.gradle | 4 ++-- materialious/electron/materialious.metainfo.xml | 6 +++++- materialious/electron/package.json | 4 ++-- materialious/package.json | 4 ++-- update_versions.py | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 6ae2853d..1c4fb2e9 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 159 - versionName "1.10.18" + versionCode 160 + versionName "1.11.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 f945386d..03597390 100644 --- a/materialious/electron/materialious.metainfo.xml +++ b/materialious/electron/materialious.metainfo.xml @@ -69,7 +69,11 @@ - + + + https://github.com/Materialious/Materialious/releases/tag/1.11.0 + + https://github.com/Materialious/Materialious/releases/tag/1.10.18 diff --git a/materialious/electron/package.json b/materialious/electron/package.json index e7534f19..04105ab0 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -1,6 +1,6 @@ { "name": "Materialious", - "version": "1.10.18", + "version": "1.11.0", "description": "Modern material design for Invidious.", "author": { "name": "Ward Pearce", @@ -44,4 +44,4 @@ "capacitor", "electron" ] -} +} \ No newline at end of file diff --git a/materialious/package.json b/materialious/package.json index 2ef59008..2e8c5a67 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -1,6 +1,6 @@ { "name": "materialious", - "version": "1.10.18", + "version": "1.11.0", "private": true, "scripts": { "dev": "vite dev", @@ -70,4 +70,4 @@ "svelte-persisted-store": "^0.12.0", "youtubei.js": "^16.0.1" } -} +} \ No newline at end of file diff --git a/update_versions.py b/update_versions.py index 5f7b70db..de60ec1e 100644 --- a/update_versions.py +++ b/update_versions.py @@ -3,7 +3,7 @@ import os import re from datetime import datetime -LATEST_VERSION = "1.10.18" +LATEST_VERSION = "1.11.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 08cf47ce5f8a8e5b870f22ca80ea6e4c8f931c0a Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 4 Nov 2025 20:48:44 +1300 Subject: [PATCH 2/8] Remove unused function proxyVideoUrl --- materialious/src/lib/misc.ts | 7 ------- materialious/src/lib/timestamps.ts | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 2ed670a6..68dab9a4 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -23,13 +23,6 @@ export function decodeHtmlCharCodes(str: string): string { return decode(str); } -export function proxyVideoUrl(source: string): string { - const rawSrc = new URL(source); - rawSrc.host = get(instanceStore).replace('http://', '').replace('https://', ''); - - return rawSrc.toString(); -} - export function unsafeRandomItem(array: any[]): any { return array[Math.floor(Math.random() * array.length)]; } diff --git a/materialious/src/lib/timestamps.ts b/materialious/src/lib/timestamps.ts index f675bc03..dc28a1c7 100644 --- a/materialious/src/lib/timestamps.ts +++ b/materialious/src/lib/timestamps.ts @@ -55,7 +55,7 @@ export function phaseDescription( // Handle youtubejs timestamps if ( fallbackPatch === 'youtubejs' && - href.includes(`https://www.youtube.com/watch?v=${videoId}&t=`) + href.includes(`https://www.youtube.com/watch?v=${videoId}`) ) { const url = new URL(href); const timeParam = url.searchParams.get('t') || '0'; From 3f7981b93c836267bd2e98dffd277ad14abfa2a7 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 4 Nov 2025 20:52:16 +1300 Subject: [PATCH 3/8] Go directly to video if Video ID provided in search --- materialious/src/lib/components/Search.svelte | 16 ++++++++++++---- materialious/src/lib/misc.ts | 5 +++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/materialious/src/lib/components/Search.svelte b/materialious/src/lib/components/Search.svelte index 3c22de02..0116af20 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -10,6 +10,7 @@ isAndroidTvStore, searchHistoryStore } from '../store'; + import { isVideoID } from '$lib/misc'; const dispatch = createEventDispatcher(); @@ -39,20 +40,27 @@ function handleSubmit(event: Event | undefined = undefined) { if (event) event.preventDefault(); - if (search.trim() === '') return; + const searchTrimed = search.trim(); + + if (searchTrimed) return; + + if (isVideoID(searchTrimed)) { + // Go directly to video if Video ID provided + goto(`/watch/${searchTrimed}`); + } selectedSuggestionIndex = -1; - goto(`/search/${encodeURIComponent(search)}`); + goto(`/search/${encodeURIComponent(searchTrimed)}`); suggestionsForSearch = []; showSearchBox = false; - if ($interfaceSearchHistoryEnabled && !$searchHistoryStore.includes(search)) { + if ($interfaceSearchHistoryEnabled && !$searchHistoryStore.includes(searchTrimed)) { let pastHistory = $searchHistoryStore; if (pastHistory.length > 15) { pastHistory.pop(); } - searchHistoryStore.set([search, ...pastHistory]); + searchHistoryStore.set([searchTrimed, ...pastHistory]); } } diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index 68dab9a4..069c1b5f 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -14,6 +14,11 @@ import type { VideoBase } from './api/model'; +export function isVideoID(videoId: string): boolean { + var regExp = /^[a-zA-Z0-9_-]{11}$/; + return regExp.test(videoId); +} + export function truncate(value: string, maxLength: number = 50): string { return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value; } From 509eea83deeac9825c51fc119f95d3a018553539 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 4 Nov 2025 22:36:35 +1300 Subject: [PATCH 4/8] Improved premiere handling --- materialious/electron/package-lock.json | 4 +- materialious/electron/package.json | 2 +- materialious/package-lock.json | 11 ++++- materialious/package.json | 3 +- materialious/src/lib/api/model.ts | 2 +- materialious/src/lib/components/Player.svelte | 11 +---- materialious/src/lib/components/Search.svelte | 8 ++-- materialious/src/lib/css/global.css | 9 ++++ materialious/src/lib/i18n/index.ts | 14 +++++- materialious/src/lib/i18n/locales/en.json | 2 +- materialious/src/lib/patches/youtubejs.ts | 6 +-- materialious/src/lib/time.ts | 32 ++++++++++++++ materialious/src/lib/watch.ts | 4 -- .../routes/(app)/watch/[slug]/+page.svelte | 43 +++++++++++++++++-- 14 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 materialious/src/lib/time.ts diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index 601e6e53..eb3b1080 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.10.18", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.10.18", + "version": "1.11.0", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/package.json b/materialious/electron/package.json index 04105ab0..75f5d9e9 100644 --- a/materialious/electron/package.json +++ b/materialious/electron/package.json @@ -44,4 +44,4 @@ "capacitor", "electron" ] -} \ No newline at end of file +} diff --git a/materialious/package-lock.json b/materialious/package-lock.json index 4d04842a..97446ac4 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -1,12 +1,12 @@ { "name": "materialious", - "version": "1.10.18", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "materialious", - "version": "1.10.18", + "version": "1.11.0", "hasInstallScript": true, "dependencies": { "@capacitor-community/electron": "^5.0.1", @@ -20,6 +20,7 @@ "beercss": "^3.12.11", "bgutils-js": "^3.2.0", "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", + "dayjs": "^1.11.19", "fuse.js": "^7.0.0", "googlevideo": "^4.0.4", "he": "^1.2.0", @@ -5235,6 +5236,12 @@ "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/materialious/package.json b/materialious/package.json index 2e8c5a67..e729968b 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -53,6 +53,7 @@ "beercss": "^3.12.11", "bgutils-js": "^3.2.0", "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", + "dayjs": "^1.11.19", "fuse.js": "^7.0.0", "googlevideo": "^4.0.4", "he": "^1.2.0", @@ -70,4 +71,4 @@ "svelte-persisted-store": "^0.12.0", "youtubei.js": "^16.0.1" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 3dd8672a..a3cf0683 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -38,7 +38,7 @@ export interface Video extends VideoBase { descriptionHtml: string; published: number; publishedText: string; - premiereTimestamp: number; + premiereTimestamp?: number; liveNow: boolean; premium: boolean; isUpcoming: boolean; diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index dd651b51..c68af0ce 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -863,7 +863,7 @@ {#if showVideoRetry} -
+
{#if $playerYouTubeJsFallback}

{$_('player.youtubeJsLoading')}

@@ -930,13 +930,4 @@ .hide { display: none; } - - .fallback { - height: 30vh; - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - } diff --git a/materialious/src/lib/components/Search.svelte b/materialious/src/lib/components/Search.svelte index 0116af20..1a38fb7a 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -11,6 +11,7 @@ searchHistoryStore } from '../store'; import { isVideoID } from '$lib/misc'; + import { resolve } from '$app/paths'; const dispatch = createEventDispatcher(); @@ -38,15 +39,16 @@ } function handleSubmit(event: Event | undefined = undefined) { - if (event) event.preventDefault(); + event?.preventDefault(); const searchTrimed = search.trim(); - if (searchTrimed) return; + if (!searchTrimed) return; if (isVideoID(searchTrimed)) { // Go directly to video if Video ID provided - goto(`/watch/${searchTrimed}`); + goto(resolve('/watch/[videoId]', { videoId: searchTrimed })); + return; } selectedSuggestionIndex = -1; diff --git a/materialious/src/lib/css/global.css b/materialious/src/lib/css/global.css index 5f711192..b10b2b97 100644 --- a/materialious/src/lib/css/global.css +++ b/materialious/src/lib/css/global.css @@ -51,6 +51,15 @@ dialog { padding: 1em !important; } +.video-placeholder { + height: 30vh; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + @media screen and (max-width: 1000px) { menu.mobile { position: fixed !important; diff --git a/materialious/src/lib/i18n/index.ts b/materialious/src/lib/i18n/index.ts index 213a0cbb..b03c0e4f 100644 --- a/materialious/src/lib/i18n/index.ts +++ b/materialious/src/lib/i18n/index.ts @@ -1,6 +1,8 @@ import i18next, { type InitOptions } from 'i18next'; import { writable, type Writable } from 'svelte/store'; +import dayjs from 'dayjs'; + const defaultLocale = 'en'; export const locale: Writable = writable(defaultLocale); @@ -40,6 +42,13 @@ function getUserLocale(): string { return defaultLocale; } +async function loadDayjsLocale(localeName: string) { + try { + const { default: locale } = await import(`dayjs/locale/${localeName}`); + dayjs.locale(locale); + } catch {} +} + export async function initI18n(selectedLocale: string = getUserLocale()): Promise { const langToLoad = resources[selectedLocale] ? selectedLocale : defaultLocale; const translations = await resources[langToLoad](); @@ -63,8 +72,11 @@ export async function initI18n(selectedLocale: string = getUserLocale()): Promis locale.set(langToLoad); _.set(i18next.t.bind(i18next)); - window.addEventListener('languagechange', () => { + await loadDayjsLocale(selectedLocale); + + window.addEventListener('languagechange', async () => { const newLang = getUserLocale(); initI18n(newLang); + await loadDayjsLocale(newLang); }); } diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 60edacb8..aedfc196 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -2,7 +2,6 @@ "enabled": "Enabled", "disabled": "Disabled", "premium": "Premium YouTube content can't be watched on Materialious.", - "isUpcoming": "Video is not in a watchable state yet.", "copyUrl": "Copy URL", "loadMore": "Load more", "views": "views", @@ -82,6 +81,7 @@ "pauseTimer": "Pause timer", "pauseVideoIn": "Pause video in ", "preferredQuality": "Preferred quality", + "premiere": "Video will be available", "chapters": "Chapters", "share": { "title": "Share", diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index 341e23e0..9d1e94a2 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -153,8 +153,6 @@ export async function patchYoutubeJs(videoId: string): Promise { return; } - console.log(recommended); - recommendedVideos.push({ videoThumbnails: (recommended?.content_image?.image as Thumbnail[]) || [], videoId: recommended?.content_id || '', @@ -233,7 +231,9 @@ export async function patchYoutubeJs(videoId: string): Promise { descriptionHtml: video.secondary_info.description?.toHTML() || descString, published: 0, publishedText: video.primary_info.published?.toString() || '', - premiereTimestamp: 0, + premiereTimestamp: video.basic_info.is_upcoming + ? Math.floor(video.basic_info.start_timestamp?.getTime() || 0 / 1000) + : undefined, hlsUrl: video.streaming_data?.hls_manifest_url || undefined, liveNow: video.basic_info.is_live || false, // @ts-expect-error Type does have offer_id diff --git a/materialious/src/lib/time.ts b/materialious/src/lib/time.ts new file mode 100644 index 00000000..5c31c56d --- /dev/null +++ b/materialious/src/lib/time.ts @@ -0,0 +1,32 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; + +dayjs.extend(utc); +dayjs.extend(duration); +dayjs.extend(relativeTime); +dayjs.extend(advancedFormat); + +export function humanFriendlyTimestamp(utcTimestamp: number): string { + const now = dayjs(); + const timestamp = dayjs.utc(utcTimestamp).local(); + + const isSameDay = now.isSame(timestamp, 'day'); + const isSameMonth = now.isSame(timestamp, 'month'); + const isThisYear = now.isSame(timestamp, 'year'); + + const diffMilliseconds = timestamp.diff(now); + const diffDuration = dayjs.duration(diffMilliseconds); + + if (isSameDay) { + return diffDuration.humanize(true); + } else if (isSameMonth) { + return timestamp.format('Do @ h:mm A'); + } else if (isThisYear) { + return timestamp.format('MMMM Do @ h:mm A'); + } else { + return timestamp.format('MMMM Do YYYY h:mm A'); + } +} diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index d9ff42d8..38299bee 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -30,10 +30,6 @@ export async function getWatchDetails(videoId: string, url: URL) { error(400, get(_)('premium')); } - if (video.isUpcoming) { - error(400, get(_)('isUpcoming')); - } - let personalPlaylists; if (get(authStore)) { postHistory(video.videoId); diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index c2b8c724..499b5c36 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -36,6 +36,9 @@ import LikesDislikes from '$lib/components/watch/LikesDislikes.svelte'; import Comment from '$lib/components/watch/Comment.svelte'; import { expandSummery } from '$lib/misc'; + import { humanFriendlyTimestamp } from '$lib/time.js'; + import { getWatchDetails } from '$lib/watch.js'; + import { page } from '$app/state'; let { data = $bindable() } = $props(); @@ -65,6 +68,9 @@ let playerCurrentTime: number = $state(0); + let premiereTime = $state(''); + let premiereUpdateInterval: NodeJS.Timeout; + $effect(() => { if ($interfaceAutoExpandComments && comments) { expandSummery('comment-section'); @@ -231,6 +237,19 @@ playerCurrentTime = playerElement.currentTime; }); } + + if (data.video.premiereTimestamp) { + premiereTime = humanFriendlyTimestamp(data.video.premiereTimestamp); + premiereUpdateInterval = setInterval(async () => { + data = await getWatchDetails(data.video.videoId, page.url); + + if (data.video.premiereTimestamp) { + premiereTime = humanFriendlyTimestamp(data.video.premiereTimestamp); + } else { + clearInterval(premiereUpdateInterval); + } + }, 60000); + } }); onDestroy(() => { @@ -240,6 +259,10 @@ if (pauseTimeout) { clearTimeout(pauseTimeout); } + + if (premiereUpdateInterval) { + clearInterval(premiereUpdateInterval); + } }); async function goToCurrentPlaylistItem() { @@ -319,9 +342,23 @@
- {#key data.video.videoId} - - {/key} + {#if !data.video.premiereTimestamp} + {#key data.video.videoId} + + {/key} + {:else} +
+

{$_('player.premiere')}

+
+ {premiereTime} +
+
+ {/if}
{letterCase(data.video.title)}
From a2216ed3fc31cd4b5dc02f9fcace9ec65e9f0df4 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Tue, 4 Nov 2025 22:50:31 +1300 Subject: [PATCH 5/8] Improved no result placeholder --- materialious/src/lib/components/ItemsList.svelte | 12 ++++++++++++ materialious/src/lib/i18n/locales/en.json | 1 + materialious/src/routes/(app)/+page.svelte | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/materialious/src/lib/components/ItemsList.svelte b/materialious/src/lib/components/ItemsList.svelte index 15fb6f89..5b2000f9 100644 --- a/materialious/src/lib/components/ItemsList.svelte +++ b/materialious/src/lib/components/ItemsList.svelte @@ -221,6 +221,18 @@
+ {#if items.length === 0} +
+
+ +

{$_('noResult')}

+
+ {/if}
{#each items as item, index} diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index aedfc196..47179320 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -1,6 +1,7 @@ { "enabled": "Enabled", "disabled": "Disabled", + "popularPageDisabled": "Popular page has been disabled by Admins", "premium": "Premium YouTube content can't be watched on Materialious.", "copyUrl": "Copy URL", "loadMore": "Load more", diff --git a/materialious/src/routes/(app)/+page.svelte b/materialious/src/routes/(app)/+page.svelte index d8af57ba..364bf757 100644 --- a/materialious/src/routes/(app)/+page.svelte +++ b/materialious/src/routes/(app)/+page.svelte @@ -1,14 +1,23 @@ {#if data.popularDisabled} - +
+
+ +

{$_('disabled')}

+

{$_('popularPageDisabled')}

+
{:else} {/if} From 0d3011dacd1b31d5ff24875d22af1404c5f9f90b Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 5 Nov 2025 00:26:34 +1300 Subject: [PATCH 6/8] Improved search filtering --- materialious/src/lib/api/index.ts | 17 +- .../src/lib/components/ItemsList.svelte | 4 +- .../src/lib/components/Thumbnail.svelte | 2 - materialious/src/lib/i18n/locales/en.json | 8 + .../routes/(app)/search/[slug]/+page.svelte | 162 ++++++++++++------ 5 files changed, 135 insertions(+), 58 deletions(-) diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index c3e40d9d..6a3e1705 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -179,13 +179,18 @@ export async function getHashtag(tag: string, page: number = 0): Promise<{ resul return await resp.json(); } +export interface SearchOptions { + sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'; + type?: 'video' | 'playlist' | 'channel' | 'all'; + duration?: 'short' | 'medium' | 'long'; + date?: 'hour' | 'today' | 'week' | 'month' | 'year'; + features?: string; + page?: string; +} + export async function getSearch( search: string, - options: { - sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'; - type?: 'video' | 'playlist' | 'channel' | 'all'; - page?: string; - }, + options: SearchOptions, fetchOptions?: RequestInit ): Promise<(Channel | Video | Playlist | HashTag)[]> { if (typeof options.sort_by === 'undefined') { @@ -193,7 +198,7 @@ export async function getSearch( } if (typeof options.type === 'undefined') { - options.type = 'video'; + options.type = 'all'; } if (typeof options.page === 'undefined') { diff --git a/materialious/src/lib/components/ItemsList.svelte b/materialious/src/lib/components/ItemsList.svelte index 5b2000f9..fa23bca5 100644 --- a/materialious/src/lib/components/ItemsList.svelte +++ b/materialious/src/lib/components/ItemsList.svelte @@ -263,7 +263,9 @@ }} > {#if item.type === 'video' || item.type === 'shortVideo' || item.type === 'stream'} - + {#key item.videoId} + + {/key} {#if $authStore && decodeURIComponent($authStore.username) === playlistAuthor && 'indexId' in item}
+ + +
+
+ {#each filters as filter} +
+
{filter.title}
+
    + {#each filter.options as filterOption} +
  • + +
  • + {/each} +
+
+ {/each} +
+
+ + +
{#if $searchCacheStore[data.searchStoreId]} From 9ef9d1bc79b7511c581c95c7fb1c68e9982740f2 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 5 Nov 2025 01:07:36 +1300 Subject: [PATCH 7/8] Improved playlist --- materialious/electron/src/setup.ts | 29 +++++++ materialious/src/lib/components/Player.svelte | 66 +++------------ materialious/src/lib/i18n/locales/en.json | 4 +- materialious/src/lib/player.ts | 80 +++++++++++++++++++ materialious/src/lib/store.ts | 2 + .../routes/(app)/watch/[slug]/+page.svelte | 19 ++++- 6 files changed, 144 insertions(+), 56 deletions(-) diff --git a/materialious/electron/src/setup.ts b/materialious/electron/src/setup.ts index ae6fa10d..6af16323 100644 --- a/materialious/electron/src/setup.ts +++ b/materialious/electron/src/setup.ts @@ -5,6 +5,7 @@ import { setupCapacitorElectronPlugins } from '@capacitor-community/electron'; import { USER_AGENT } from 'bgutils-js'; +import path from 'node:path'; import chokidar from 'chokidar'; import type { MenuItemConstructorOptions } from 'electron'; import { @@ -219,6 +220,34 @@ export class ElectronCapacitorApp { CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', ''); }, 400); }); + + // remove so we can register each time as we run the app. + app.removeAsDefaultProtocolClient('materialious'); + + // If we are running a non-packaged version of the app && on windows + if (process.env.NODE_ENV === 'development' && process.platform === 'win32') { + // Set the path of electron.exe and your app. + // These two additional parameters are only available on windows. + app.setAsDefaultProtocolClient('materialious', process.execPath, [ + path.resolve(process.argv[1]) + ]); + } else { + app.setAsDefaultProtocolClient('materialious'); + } + + const gotTheLock = app.requestSingleInstanceLock(); + + if (!gotTheLock) { + app.quit(); + } else { + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (this.MainWindow) { + if (this.MainWindow.isMinimized()) this.MainWindow.restore(); + this.MainWindow.focus(); + } + }); + } } } diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index c68af0ce..6c2c5b52 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -47,7 +47,7 @@ } from '../store'; import { getDynamicTheme, setStatusBarColor } from '../theme'; import { patchYoutubeJs } from '$lib/patches/youtubejs'; - import { playbackRates } from '$lib/player'; + import { goToNextVideo, goToPreviousVideo, playbackRates } from '$lib/player'; import { EndTimeElement } from '$lib/shaka-elements/endTime'; import { loadEntirePlaylist } from '$lib/playlist'; import { goto } from '$app/navigation'; @@ -602,6 +602,16 @@ playerElement.pause(); playerElement.currentTime = 0; }); + + if (data.playlistId) { + navigator.mediaSession.setActionHandler('previoustrack', () => { + goToPreviousVideo(data.playlistId); + }); + + navigator.mediaSession.setActionHandler('nexttrack', async () => { + await goToNextVideo(data.video, data.playlistId); + }); + } } Mousetrap.bind('space', () => { @@ -690,59 +700,7 @@ }); playerElement?.addEventListener('ended', async () => { - if (!data.playlistId) { - if ($playerAutoplayNextByDefaultStore) { - goto( - `/${$isAndroidTvStore ? 'tv' : 'watch'}/${data.video.recommendedVideos[0].videoId}`, - { replaceState: $isAndroidTvStore } - ); - } - - return; - } - - const playlist = await loadEntirePlaylist(data.playlistId); - const playlistVideoIds = playlist.videos.map((value) => { - return value.videoId; - }); - - let goToVideo: PlaylistPageVideo | undefined; - - const shufflePlaylist = $playlistSettingsStore[data.playlistId]?.shuffle ?? false; - const loopPlaylist = $playlistSettingsStore[data.playlistId]?.loop ?? false; - - if (shufflePlaylist) { - goToVideo = unsafeRandomItem(playlist.videos); - } else { - const currentVideoIndex = playlistVideoIds.indexOf(data.video.videoId); - const newIndex = currentVideoIndex + 1; - if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) { - goToVideo = playlist.videos[newIndex]; - } else if (loopPlaylist) { - // Loop playlist on end - goToVideo = playlist.videos[0]; - } - } - - if (typeof goToVideo !== 'undefined') { - if ($syncPartyConnectionsStore) { - $syncPartyConnectionsStore.forEach((conn) => { - if (typeof goToVideo === 'undefined') return; - - conn.send({ - events: [ - { type: 'change-video', videoId: goToVideo.videoId }, - { type: 'playlist', playlistId: data.playlistId } - ] - } as PlayerEvents); - }); - } - - goto( - `/${$isAndroidTvStore ? 'tv' : 'watch'}/${goToVideo.videoId}?playlist=${data.playlistId}`, - { replaceState: $isAndroidTvStore } - ); - } + await goToNextVideo(data.video, data.playlistId); }); try { diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index 163ecba7..51de37fa 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -68,7 +68,9 @@ "private": "Private", "playVideos": "Play videos", "shuffleVideos": "Play videos shuffled", - "loopPlaylist": "Loop playlist" + "loopPlaylist": "Loop playlist", + "next": "Next", + "previous": "Previous" }, "thumbnail": { "live": "LIVE", diff --git a/materialious/src/lib/player.ts b/materialious/src/lib/player.ts index cbb32cbc..3c879b09 100644 --- a/materialious/src/lib/player.ts +++ b/materialious/src/lib/player.ts @@ -1,3 +1,17 @@ +import { get } from 'svelte/store'; +import type { PlaylistPageVideo, VideoPlay } from './api/model'; +import { + isAndroidTvStore, + playerAutoplayNextByDefaultStore, + playerPlaylistHistory, + playlistSettingsStore, + syncPartyConnectionsStore +} from './store'; +import { goto } from '$app/navigation'; +import { loadEntirePlaylist } from './playlist'; +import { unsafeRandomItem } from './misc'; +import { resolve } from '$app/paths'; + export interface PlayerEvent { type: 'pause' | 'seek' | 'change-video' | 'play' | 'playlist' | 'goto'; path?: string; @@ -11,3 +25,69 @@ export interface PlayerEvents { } export const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3]; + +export function goToPreviousVideo(playlistId: string | null) { + const previousVideos = get(playerPlaylistHistory); + if (previousVideos.length > 1) { + goto( + resolve('/watch/[videoId]?playlist=[playlistId]', { + videoId: previousVideos[1], + playlistId: playlistId ?? '' + }) + ); + delete previousVideos[1]; + } +} + +export async function goToNextVideo(video: VideoPlay, playlistId: string | null) { + const isAndroidTv = get(isAndroidTvStore); + + if (!playlistId) { + if (get(playerAutoplayNextByDefaultStore)) { + goto(`/${isAndroidTv ? 'tv' : 'watch'}/${video.recommendedVideos[0].videoId}`, { + replaceState: isAndroidTv + }); + } + return; + } + + const playlist = await loadEntirePlaylist(playlistId); + const playlistVideoIds = playlist.videos.map((value) => { + return value.videoId; + }); + + let goToVideo: PlaylistPageVideo | undefined; + + const shufflePlaylist = get(playlistSettingsStore)[playlistId]?.shuffle ?? false; + const loopPlaylist = get(playlistSettingsStore)[playlistId]?.loop ?? false; + + if (shufflePlaylist) { + goToVideo = unsafeRandomItem(playlist.videos); + } else { + const currentVideoIndex = playlistVideoIds.indexOf(video.videoId); + const newIndex = currentVideoIndex + 1; + if (currentVideoIndex !== -1 && newIndex < playlistVideoIds.length) { + goToVideo = playlist.videos[newIndex]; + } else if (loopPlaylist) { + // Loop playlist on end + goToVideo = playlist.videos[0]; + } + } + + if (typeof goToVideo !== 'undefined') { + get(syncPartyConnectionsStore)?.forEach((conn) => { + if (typeof goToVideo === 'undefined') return; + + conn.send({ + events: [ + { type: 'change-video', videoId: goToVideo.videoId }, + { type: 'playlist', playlistId: playlistId } + ] + } as PlayerEvents); + }); + + goto(`/${isAndroidTv ? 'tv' : 'watch'}/${goToVideo.videoId}?playlist=${playlistId}`, { + replaceState: isAndroidTv + }); + } +} diff --git a/materialious/src/lib/store.ts b/materialious/src/lib/store.ts index 286f2786..ff229405 100644 --- a/materialious/src/lib/store.ts +++ b/materialious/src/lib/store.ts @@ -58,6 +58,8 @@ export const playerCCByDefault = persisted('CCByDefault', false); export const playerDefaultPlaybackSpeed: Writable = persisted('defaultPlaybackSpeed', 1); export const playerStatisticsByDefault = persisted('playerStatistics', false); +export const playerPlaylistHistory: Writable = writable([]); + export const returnYtDislikesStore = persisted('returnYtDislikes', false); export const returnYTDislikesInstanceStore: Writable = persisted( 'returnYTDislikesInstance', diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index 499b5c36..130ac3d4 100644 --- a/materialious/src/routes/(app)/watch/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/watch/[slug]/+page.svelte @@ -13,11 +13,12 @@ import { getBestThumbnail } from '$lib/images'; import { letterCase } from '$lib/letterCasing'; import { cleanNumber, humanizeSeconds, numberWithCommas } from '$lib/numbers'; - import type { PlayerEvents } from '$lib/player'; + import { goToNextVideo, goToPreviousVideo, type PlayerEvents } from '$lib/player'; import { authStore, interfaceAutoExpandChapters, interfaceAutoExpandComments, + playerPlaylistHistory, playerTheatreModeByDefaultStore, playlistCacheStore, playlistSettingsStore, @@ -219,6 +220,7 @@ onMount(async () => { if (data.playlistId) { await goToCurrentPlaylistItem(); + playerPlaylistHistory.set([data.video.videoId, ...$playerPlaylistHistory]); } if ($interfaceAutoExpandChapters) { @@ -573,6 +575,21 @@ {$_('playlist.shuffleVideos')}
+ +
From 15d0f5219d24e8cf602ff343b5b1c65f74a50ecb Mon Sep 17 00:00:00 2001 From: WardPearce Date: Wed, 5 Nov 2025 01:11:10 +1300 Subject: [PATCH 8/8] Only build on osx-x64 --- .github/workflows/prod-desktop.yml | 9 ---- .github/workflows/pull-desktop.yml | 73 +++++++++++++----------------- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/.github/workflows/prod-desktop.yml b/.github/workflows/prod-desktop.yml index 9a8c1405..e1ac1627 100644 --- a/.github/workflows/prod-desktop.yml +++ b/.github/workflows/prod-desktop.yml @@ -19,20 +19,11 @@ jobs: matrix: node-version: [latest] runtime: - - linux-x64 - - linux-arm64 - - win-x64 - osx-x64 include: - - runtime: linux-x64 - os: ubuntu-latest - - runtime: linux-arm64 - os: ubuntu-latest - runtime: osx-x64 os: macOS-latest - - runtime: win-x64 - os: windows-latest runs-on: ${{ matrix.os }} diff --git a/.github/workflows/pull-desktop.yml b/.github/workflows/pull-desktop.yml index b5fb836b..6b2dfd7a 100644 --- a/.github/workflows/pull-desktop.yml +++ b/.github/workflows/pull-desktop.yml @@ -1,59 +1,50 @@ name: Check desktop on: - pull_request: - branches: ["main"] + pull_request: + branches: ["main"] - workflow_dispatch: + workflow_dispatch: permissions: - contents: write + contents: write jobs: - desktop-check: - strategy: - matrix: - node-version: [latest] - runtime: - - linux-x64 - - linux-arm64 - - win-x64 - - osx-x64 + desktop-check: + strategy: + matrix: + node-version: [latest] + runtime: + - osx-x64 - include: - - runtime: linux-x64 - os: ubuntu-latest - - runtime: linux-arm64 - os: ubuntu-latest - - runtime: osx-x64 - os: macOS-latest - - runtime: win-x64 - os: windows-latest + include: + - runtime: osx-x64 + os: macOS-latest - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 + steps: + - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} - - name: Install modules - run: cd ./materialious && npm install + - name: Install modules + run: cd ./materialious && npm install - - name: Build package - run: cd ./materialious && npm run build + - name: Build package + run: cd ./materialious && npm run build - - name: Sync electron package - run: cd ./materialious && npx cap sync @capacitor-community/electron + - name: Sync electron package + run: cd ./materialious && npx cap sync @capacitor-community/electron - - name: Patch capacitor plugin - run: cd ./materialious/electron && python patch_capacitor_plugin.py + - name: Patch capacitor plugin + run: cd ./materialious/electron && python patch_capacitor_plugin.py - - name: Install electron modules - run: cd ./materialious/electron && npm install + - name: Install electron modules + run: cd ./materialious/electron && npm install - - name: Build desktop packages - run: cd ./materialious/electron && npm run electron:make-no-publish + - name: Build desktop packages + run: cd ./materialious/electron && npm run electron:make-no-publish