Merge pull request #1171 from Materialious/update/1.11.0

Update/1.11.0
This commit is contained in:
Ward
2025-11-04 12:16:39 +00:00
committed by GitHub
29 changed files with 479 additions and 213 deletions
-9
View File
@@ -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 }}
+32 -41
View File
@@ -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
+2 -2
View File
@@ -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.
@@ -69,7 +69,11 @@
<releases>
<release version="1.10.18" date="2025-11-02">
<release version="1.11.0" date="2025-11-04">
<url>https://github.com/Materialious/Materialious/releases/tag/1.11.0</url>
</release>
<release version="1.10.18" date="2025-11-02">
<url>https://github.com/Materialious/Materialious/releases/tag/1.10.18</url>
</release>
<release version="1.10.17" date="2025-10-16">
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.10.18",
"version": "1.11.0",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
+29
View File
@@ -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();
}
});
}
}
}
+9 -2
View File
@@ -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",
+2 -1
View File
@@ -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",
+11 -6
View File
@@ -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') {
+1 -1
View File
@@ -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;
@@ -221,6 +221,18 @@
</script>
<div class={classes} class:android-container={$isAndroidTvStore}>
{#if items.length === 0}
<div class="space"></div>
<div class="center-align">
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
><path
style="fill: var(--secondary);"
d="M11 0h2v4h-2zM5.001.586L3.587 2L6.5 4.914L7.915 3.5zM1 13.734L5.42 6h13.16L23 13.734V22H1zM6.58 8l-2.857 5h5.178l.193.75a3.002 3.002 0 0 0 5.812 0l.193-.75h5.178L17.42 8zM18.999.586L16.085 3.5l1.414 1.414L20.413 2z"
/></svg
>
<h1>{$_('noResult')}</h1>
</div>
{/if}
<div class="grid" bind:this={gridElement}>
{#each items as item, index}
<ContentColumn>
@@ -251,7 +263,9 @@
}}
>
{#if item.type === 'video' || item.type === 'shortVideo' || item.type === 'stream'}
<Thumbnail video={item} {playlistId} />
{#key item.videoId}
<Thumbnail video={item} {playlistId} />
{/key}
{#if $authStore && decodeURIComponent($authStore.username) === playlistAuthor && 'indexId' in item}
<div class="right-align" style="margin: 1em .5em;">
<button
+13 -64
View File
@@ -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 {
@@ -863,7 +821,7 @@
</div>
{#if showVideoRetry}
<article class="fallback">
<article class="video-placeholder">
{#if $playerYouTubeJsFallback}
<p>{$_('player.youtubeJsLoading')}</p>
<progress class="circle large"></progress>
@@ -930,13 +888,4 @@
.hide {
display: none;
}
.fallback {
height: 30vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
+15 -5
View File
@@ -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]);
}
}
@@ -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;
+9
View File
@@ -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;
+13 -1
View File
@@ -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<string> = 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<void> {
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);
});
}
+13 -2
View File
@@ -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",
+5 -7
View File
@@ -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)];
}
+3 -3
View File
@@ -153,8 +153,6 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
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<VideoPlay> {
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
+80
View File
@@ -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
});
}
}
+2
View File
@@ -58,6 +58,8 @@ export const playerCCByDefault = persisted('CCByDefault', false);
export const playerDefaultPlaybackSpeed: Writable<number> = persisted('defaultPlaybackSpeed', 1);
export const playerStatisticsByDefault = persisted('playerStatistics', false);
export const playerPlaylistHistory: Writable<string[]> = writable([]);
export const returnYtDislikesStore = persisted('returnYtDislikes', false);
export const returnYTDislikesInstanceStore: Writable<string | null | undefined> = persisted(
'returnYTDislikesInstance',
+32
View File
@@ -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');
}
}
+1 -1
View File
@@ -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';
-4
View File
@@ -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);
+12 -3
View File
@@ -1,14 +1,23 @@
<script lang="ts">
import ItemsList from '$lib/components/ItemsList.svelte';
import { _ } from '$lib/i18n/index';
import { feedCacheStore } from '$lib/store';
let { data } = $props();
</script>
{#if data.popularDisabled}
<nav class="center-align">
<p>Popular page has been disabled</p>
</nav>
<div class="center-align">
<div class="space"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 24 24"
><path
style="fill: var(--secondary);"
d="M11.25 21.975Q9.3 21.825 7.612 21t-2.937-2.162q-1.25-1.338-1.963-3.1T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12v.475q-.425-.275-.988-.537t-1.037-.438q-.2-3.15-2.488-5.325T12 4q-1.4 0-2.638.45T7.1 5.7l6.125 6.125q-.475.2-.912.463t-.863.562L5.7 7.1q-.8 1.025-1.25 2.263T4 12q0 2.475 1.338 4.438T8.8 19.325q.45.7 1.125 1.425t1.325 1.225ZM17 20q1.475 0 2.738-.675T21.75 17.5q-.75-1.15-2.013-1.825T17 15q-1.475 0-2.738.675T12.25 17.5q.75 1.15 2.013 1.825T17 20Zm0 2q-2.4 0-4.288-1.263T10 17.5q.825-1.975 2.713-3.238T17 13q2.4 0 4.288 1.263T24 17.5q-.825 1.975-2.713 3.238T17 22Zm0-3q-.625 0-1.063-.438T15.5 17.5q0-.625.438-1.063T17 16q.625 0 1.063.438T18.5 17.5q0 .625-.438 1.063T17 19Z"
/></svg
>
<h1>{$_('disabled')}</h1>
<p>{$_('popularPageDisabled')}</p>
</div>
{:else}
<ItemsList items={$feedCacheStore.popular ?? []} />
{/if}
@@ -1,12 +1,6 @@
<script lang="ts">
import { getSearch } from '$lib/api';
import ChannelThumbnail from '$lib/components/ChannelThumbnail.svelte';
import ContentColumn from '$lib/components/ContentColumn.svelte';
import HashtagThumbnail from '$lib/components/HashtagThumbnail.svelte';
import { getSearch, type SearchOptions } from '$lib/api';
import PageLoading from '$lib/components/PageLoading.svelte';
import PlaylistThumbnail from '$lib/components/PlaylistThumbnail.svelte';
import Thumbnail from '$lib/components/Thumbnail.svelte';
import { extractUniqueId } from '$lib/misc';
import { feedLastItemId, searchCacheStore } from '$lib/store';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
@@ -15,9 +9,60 @@
let { data } = $props();
let currentType: 'playlist' | 'all' | 'video' | 'channel' = $state('all');
let currentPage = 1;
let filtersOpen = $state(false);
let searchOptions: SearchOptions = $state({
type: 'all',
page: currentPage.toString(),
sort_by: 'relevance'
});
const filters: {
options: string[];
title: string;
key: 'type' | 'date' | 'duration' | 'sort_by' | 'features';
}[] = $state([
{
options: ['all', 'video', 'playlist', 'channel'],
title: $_('filters.type'),
key: 'type'
},
{
options: ['hour', 'today', 'week', 'month', 'year'],
title: $_('filters.uploadDate'),
key: 'date'
},
{
options: ['short', 'medium', 'long'],
title: $_('filters.duration'),
key: 'duration'
},
{
options: ['relevance', 'rating', 'upload_date', 'view_count'],
title: $_('filters.sortBy'),
key: 'sort_by'
},
{
options: [
'hd',
'subtitles',
'creative_commons',
'3d',
'live',
'purchased',
'4k',
'360',
'location',
'hdr',
'vr180'
],
title: $_('filters.features'),
key: 'features'
}
]);
onMount(() => {
if ($feedLastItemId) {
document
@@ -28,19 +73,18 @@
}
});
async function changeType(type: 'playlist' | 'all' | 'video' | 'channel') {
currentType = type;
currentPage = 1;
data.searchStoreId = type + data.slug;
searchCacheStore.set({ [data.searchStoreId]: await getSearch(data.slug, { type: type }) });
async function updateSearch() {
data.searchStoreId = JSON.stringify(searchOptions) + data.slug;
searchCacheStore.set({ [data.searchStoreId]: await getSearch(data.slug, searchOptions) });
}
async function loadMore(event: InfiniteEvent) {
currentPage++;
const newSearch = await getSearch(data.slug, {
page: currentPage.toString(),
type: currentType
});
searchOptions = {
...searchOptions,
page: currentPage.toString()
};
const newSearch = await getSearch(data.slug, searchOptions);
if (newSearch.length === 0) {
event.detail.complete();
@@ -53,38 +97,58 @@
}
</script>
<div style="margin-bottom: 1em;">
<div class="tabs left-align min scroll">
<a class:active={currentType === 'all'} href="#all" onclick={async () => changeType('all')}>
<i>home</i>
<span>{$_('videoTabs.all')}</span>
</a>
<a
class:active={currentType === 'video'}
href="#videos"
onclick={async () => changeType('video')}
>
<i>movie</i>
<span>{$_('videoTabs.videos')}</span>
</a>
<a
class:active={currentType === 'playlist'}
href="#playlists"
onclick={async () => changeType('playlist')}
>
<i>playlist_add_check</i>
<span>{$_('videoTabs.playlists')}</span>
</a>
<a
class:active={currentType === 'channel'}
href="#channels"
onclick={async () => changeType('channel')}
>
<i>person</i>
<span>{$_('videoTabs.channels')}</span>
</a>
</div>
</div>
<details open={filtersOpen}>
<summary>
<nav>
<button
class="secondary"
onclick={() => {
filtersOpen = !filtersOpen;
}}
>
<i>filter_alt</i>
<span>{$_('filters.filters')}</span>
</button>
</nav>
</summary>
<article class="scroll medium">
<div class="grid">
{#each filters as filter}
<div class="s12 m2 l2">
<h6>{filter.title}</h6>
<ul class="list no-margin no-padding">
{#each filter.options as filterOption}
<li>
<button
class="small border"
style="text-transform: capitalize;"
disabled={filter.key !== 'type' &&
!['all', 'video', undefined].includes(searchOptions.type)}
class:active={filterOption === searchOptions[filter.key]}
onclick={async () => {
if (filterOption === searchOptions[filter.key]) {
delete searchOptions[filter.key];
} else {
searchOptions = {
...searchOptions,
[filter.key]: filterOption
};
}
await updateSearch();
}}
>
<span>{filterOption.replaceAll('_', ' ')}</span>
</button>
</li>
{/each}
</ul>
</div>
{/each}
</div>
</article>
</details>
<div class="space"></div>
{#if $searchCacheStore[data.searchStoreId]}
<ItemsList items={$searchCacheStore[data.searchStoreId]} />
@@ -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 @@
<div class="grid">
<div class={`s12 m12 l${theatreMode ? '12' : '9'}`}>
<div style="display: flex;justify-content: center;">
{#key data.video.videoId}
<Player bind:playerElement bind:segments {data} isSyncing={$syncPartyPeerStore !== null} />
{/key}
{#if !data.video.premiereTimestamp}
{#key data.video.videoId}
<Player
bind:playerElement
bind:segments
{data}
isSyncing={$syncPartyPeerStore !== null}
/>
{/key}
{:else}
<article class="video-placeholder">
<p>{$_('player.premiere')}</p>
<h6 class="no-margin no-padding">
{premiereTime}
</h6>
</article>
{/if}
</div>
<h5>{letterCase(data.video.title)}</h5>
@@ -536,6 +575,21 @@
{$_('playlist.shuffleVideos')}
</div>
</button>
<button class="circle fill" onclick={() => goToPreviousVideo(data.playlistId)}>
<i>skip_previous</i>
<div class="tooltip bottom">
{$_('playlist.previous')}
</div>
</button>
<button
class="circle fill"
onclick={async () => await goToNextVideo(data.video, data.playlistId)}
>
<i>skip_next</i>
<div class="tooltip bottom">
{$_('playlist.next')}
</div>
</button>
</nav>
<div class="space"></div>
+1 -1
View File
@@ -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")