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 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-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 e7534f19..75f5d9e9 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", 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/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 2ef59008..e729968b 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", @@ -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", 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/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/ItemsList.svelte b/materialious/src/lib/components/ItemsList.svelte index 15fb6f89..fa23bca5 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} @@ -251,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}
{#if showVideoRetry} -
+
{#if $playerYouTubeJsFallback}

{$_('player.youtubeJsLoading')}

@@ -930,13 +888,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 3c22de02..1a38fb7a 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -10,6 +10,8 @@ isAndroidTvStore, searchHistoryStore } from '../store'; + import { isVideoID } from '$lib/misc'; + import { resolve } from '$app/paths'; const dispatch = createEventDispatcher(); @@ -37,22 +39,30 @@ } function handleSubmit(event: Event | undefined = undefined) { - if (event) event.preventDefault(); + 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(resolve('/watch/[videoId]', { videoId: searchTrimed })); + return; + } 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/components/Thumbnail.svelte b/materialious/src/lib/components/Thumbnail.svelte index 5f9596c0..59260cf0 100644 --- a/materialious/src/lib/components/Thumbnail.svelte +++ b/materialious/src/lib/components/Thumbnail.svelte @@ -20,9 +20,7 @@ synciousInstanceStore, synciousStore } from '../store'; - import { goto } from '$app/navigation'; import { queueSyncious } from '$lib/api/apiExtended'; - import { page } from '$app/state'; interface Props { video: VideoBase | Video | Notification | PlaylistPageVideo; 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..51de37fa 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -1,8 +1,8 @@ { "enabled": "Enabled", "disabled": "Disabled", + "popularPageDisabled": "Popular page has been disabled by Admins", "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", @@ -15,6 +15,14 @@ "videos": "videos", "cancel": "Cancel", "create": "Create", + "filters": { + "filters": "Filters", + "type": "Type", + "uploadDate": "Upload date", + "duration": "Duration", + "sortBy": "Short By", + "features": "Features" + }, "title": "Title", "searchPlaceholder": "Search", "delete": "Delete", @@ -60,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", @@ -82,6 +92,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/misc.ts b/materialious/src/lib/misc.ts index 2ed670a6..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; } @@ -23,13 +28,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/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/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/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/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'; 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)/+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} diff --git a/materialious/src/routes/(app)/search/[slug]/+page.svelte b/materialious/src/routes/(app)/search/[slug]/+page.svelte index 2bb2a709..b74892b4 100644 --- a/materialious/src/routes/(app)/search/[slug]/+page.svelte +++ b/materialious/src/routes/(app)/search/[slug]/+page.svelte @@ -1,12 +1,6 @@ - +
+ + + +
+
+ {#each filters as filter} +
+
{filter.title}
+
    + {#each filter.options as filterOption} +
  • + +
  • + {/each} +
+
+ {/each} +
+
+
+ +
{#if $searchCacheStore[data.searchStoreId]} diff --git a/materialious/src/routes/(app)/watch/[slug]/+page.svelte b/materialious/src/routes/(app)/watch/[slug]/+page.svelte index c2b8c724..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, @@ -36,6 +37,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 +69,9 @@ let playerCurrentTime: number = $state(0); + let premiereTime = $state(''); + let premiereUpdateInterval: NodeJS.Timeout; + $effect(() => { if ($interfaceAutoExpandComments && comments) { expandSummery('comment-section'); @@ -213,6 +220,7 @@ onMount(async () => { if (data.playlistId) { await goToCurrentPlaylistItem(); + playerPlaylistHistory.set([data.video.videoId, ...$playerPlaylistHistory]); } if ($interfaceAutoExpandChapters) { @@ -231,6 +239,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 +261,10 @@ if (pauseTimeout) { clearTimeout(pauseTimeout); } + + if (premiereUpdateInterval) { + clearInterval(premiereUpdateInterval); + } }); async function goToCurrentPlaylistItem() { @@ -319,9 +344,23 @@
- {#key data.video.videoId} - - {/key} + {#if !data.video.premiereTimestamp} + {#key data.video.videoId} + + {/key} + {:else} +
+

{$_('player.premiere')}

+
+ {premiereTime} +
+
+ {/if}
{letterCase(data.video.title)}
@@ -536,6 +575,21 @@ {$_('playlist.shuffleVideos')}
+ +
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")