Merge pull request #1377 from Materialious/update/1.13.14

Update/1.13.14
This commit is contained in:
Ward
2026-02-09 04:59:51 +00:00
committed by GitHub
15 changed files with 295 additions and 115 deletions
+3 -7
View File
@@ -310,7 +310,7 @@ Specifies the SponsorBlock API instance.
"sponsorBlockUrl": "https://sponsor.ajay.app"
```
<!--### SponsorBlock categories
### SponsorBlock categories
Defines which SponsorBlock categories should be skipped.
@@ -326,12 +326,8 @@ Available values include:
* `filler`
```json
"sponsorBlockCategories": [
"intro",
"outro",
"interaction"
]
```-->
"sponsorBlockCategoriesv2": {"sponsor":"manual","selfpromo":"automatic","preview":"timeline",}
```
### Display toast
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 197
versionName "1.13.13"
versionCode 198
versionName "1.13.14"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -96,7 +96,11 @@
<release version="1.13.13" date="2026-2-07">
<release version="1.13.14" date="2026-2-09">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.14</url>
</release>
<release version="1.13.13" date="2026-2-07">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.13</url>
</release>
<release version="1.13.12" date="2026-2-06">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.13",
"version": "1.13.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.13",
"version": "1.13.14",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.13.13",
"version": "1.13.14",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -45,4 +45,4 @@
"capacitor",
"electron"
]
}
}
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const CapacitorNodejs = require('../../../node_modules/capacitor-nodejs/electron/dist/plugin.js');
module.exports = {
CapacitorNodejs,
}
+2
View File
@@ -190,6 +190,8 @@ export class ElectronCapacitorApp {
globalShortcut.register('Control+Shift+I', () => {
this.MainWindow.webContents.toggleDevTools();
});
this.MainWindow.webContents.toggleDevTools();
// Security
this.MainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
+37 -2
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.13.11",
"version": "1.13.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.13.11",
"version": "1.13.14",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
@@ -65,6 +65,7 @@
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.47.0",
"svelte-awesome-color-picker": "^4.1.1",
"svelte-check": "^4.3.3",
"tslib": "^2.7.0",
"typescript": "^5.9.3",
@@ -5611,6 +5612,13 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -12540,6 +12548,33 @@
"node": ">=18"
}
},
"node_modules/svelte-awesome-color-picker": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/svelte-awesome-color-picker/-/svelte-awesome-color-picker-4.1.1.tgz",
"integrity": "sha512-k4APFGBFN8EpvUS0UJfdlqj8xDekMbqGBwnDYBK1QCY8kcTXSO8c7SitEk70MkiYOIByOgXlHcjaipy6H7dpAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"colord": "^2.9.3",
"svelte-awesome-slider": "2.0.0"
},
"engines": {
"node": ">=24"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/svelte-awesome-slider": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svelte-awesome-slider/-/svelte-awesome-slider-2.0.0.tgz",
"integrity": "sha512-YBkOdYm1Feaqsn2JkJBRs+Kc/X3Qy/3GuVmI7GmoYDjBaHkjx9uH4khTuED22z57Hg3gGWeDhp/clIjWDdLNaw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/svelte-check": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.13.13",
"version": "1.13.14",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
@@ -34,6 +34,7 @@
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.47.0",
"svelte-awesome-color-picker": "^4.1.1",
"svelte-check": "^4.3.3",
"tslib": "^2.7.0",
"typescript": "^5.9.3",
@@ -79,4 +80,4 @@
"youtubei.js": "^16.0.1",
"zod": "^4.3.6"
}
}
}
+38 -3
View File
@@ -649,6 +649,11 @@
});
playerElement = document.getElementById('player') as HTMLMediaElement;
// Enable AirPlay if supported
if (hasWebkitShowPlaybackTargetPicker(playerElement)) {
playerElement.setAttribute('x-webkit-airplay', 'allow');
}
playerElement.loop = playerLoop;
if ($playerState) {
@@ -832,6 +837,9 @@
} else {
playerElement.pause();
}
showPlayerUI();
return false;
});
@@ -839,44 +847,48 @@
Mousetrap.bind('right', () => {
if (!playerElement) return;
playerElement.currentTime = playerElement.currentTime + playerDoubleTapSeek;
showPlayerUI();
return false;
});
Mousetrap.bind('left', () => {
if (!playerElement) return;
playerElement.currentTime = playerElement.currentTime - playerDoubleTapSeek;
showPlayerUI();
return false;
});
Mousetrap.bind('enter', () => {
if (segmentManualSkip) {
skipSegment(segmentManualSkip);
showPlayerUI();
}
});
}
Mousetrap.bind('c', () => {
toggleSubtitles();
showPlayerUI();
return false;
});
Mousetrap.bind('f', () => {
toggleFullscreen();
showPlayerUI();
return false;
});
Mousetrap.bind('shift+left', () => {
if (!playerElement) return;
playerElement.playbackRate = playerElement.playbackRate - 0.25;
showPlayerUI();
return false;
});
Mousetrap.bind('shift+right', () => {
if (!playerElement) return;
playerElement.playbackRate = playerElement.playbackRate + 0.25;
showPlayerUI();
return false;
});
@@ -886,6 +898,8 @@
const currentTrack = player.getVariantTracks().find((track) => track.active);
const frameTime = 1 / (currentTrack?.frameRate || 30);
playerElement.currentTime -= frameTime;
showPlayerUI();
});
Mousetrap.bind('.', () => {
@@ -894,6 +908,8 @@
const currentTrack = player.getVariantTracks().find((track) => track.active);
const frameTime = 1 / (currentTrack?.frameRate || 30);
playerElement.currentTime += frameTime;
showPlayerUI();
});
playerVolumeElement?.addEventListener('mousewheel', (event) => {
@@ -911,6 +927,7 @@
playerCurrentPlaybackState = false;
playerIsBuffering = false;
savePlayerPos();
showPlayerUI();
});
playerElement?.addEventListener('ended', async () => {
@@ -927,6 +944,7 @@
playerElement?.addEventListener('waiting', () => {
playerCurrentPlaybackState = false;
playerIsBuffering = true;
showPlayerUI();
});
playerElement?.addEventListener('timeupdate', () => {
@@ -988,6 +1006,18 @@
playerTextTracks = player.getTextTracks();
});
function hasWebkitShowPlaybackTargetPicker(
el: HTMLMediaElement
): el is HTMLMediaElement & { webkitShowPlaybackTargetPicker: () => void } {
return typeof (el as any).webkitShowPlaybackTargetPicker === 'function';
}
function handleAirPlayClick() {
if (playerElement && hasWebkitShowPlaybackTargetPicker(playerElement)) {
playerElement.webkitShowPlaybackTargetPicker();
}
}
async function getLastPlayPos(): Promise<number> {
if (loadTimeFromUrl($page) || !$playerSavePlaybackPositionStore) return 0;
@@ -1513,6 +1543,11 @@
{/if}
</menu>
</button>
{#if playerElement && hasWebkitShowPlaybackTargetPicker(playerElement)}
<button class="inverse-primary" onclick={handleAirPlayClick} title="AirPlay">
<i>airplay</i>
</button>
{/if}
{#if document.pictureInPictureEnabled}
<button
class="inverse-primary"
@@ -8,10 +8,12 @@
import { Capacitor } from '@capacitor/core';
import ui from 'beercss';
import { iso31661 } from 'iso-3166';
import type { RgbaColor, HsvaColor, Colord } from 'colord';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { ensureNoTrailingSlash } from '../../misc';
import { ensureNoTrailingSlash, isMobile } from '../../misc';
import { getPages, type Pages } from '../../navPages';
import ColorPicker from 'svelte-awesome-color-picker';
import {
authStore,
darkModeStore,
@@ -32,6 +34,8 @@
searchHistoryStore,
themeColorStore
} from '../../store';
import { addToast } from '../Toast.svelte';
import { tick } from 'svelte';
let invidiousInstance = $state(get(instanceStore));
let region = $state(get(interfaceRegionStore));
@@ -39,12 +43,23 @@
let defaultPage = $state(get(interfaceDefaultPage));
let invalidInstance = $state(false);
let colorPickerOpen = $state(false);
let colorPickerDebounce: ReturnType<typeof setTimeout>;
async function setColor(color: any) {
const target = color.target;
const hex = (target as { value: string }).value;
setAmoledTheme();
themeColorStore.set(hex);
async function setColor(color: {
hsv: HsvaColor | null;
rgb: RgbaColor | null;
hex: string | null;
color: Colord | null;
}) {
if (!color.hex) return;
if (colorPickerDebounce) clearTimeout(colorPickerDebounce);
colorPickerDebounce = setTimeout(async () => {
themeColorStore.set(color.hex);
await tick();
setAmoledTheme();
}, 10);
}
function toggleDarkMode() {
@@ -139,8 +154,11 @@
</button>
</nav>
</form>
{#if invalidInstance}
<div style="margin-bottom: 6em;"></div>
{#if isMobile()}
{#if invalidInstance}
<div style="margin-bottom: 6em;"></div>
{/if}
{/if}
{#if Capacitor.isNativePlatform() && (invalidInstance || $interfaceAllowInsecureRequests)}
@@ -172,13 +190,27 @@
<span>{$_('layout.theme.lightMode')}</span>
{/if}
</button>
<button>
<button onclick={() => (colorPickerOpen = !colorPickerOpen)}>
<i>palette</i>
<span>{$_('layout.theme.color')}</span>
<input onchange={setColor} type="color" />
</button>
<div class="margin"></div>
{#if colorPickerOpen}
<div class="space"></div>
<div class="color-picker">
<ColorPicker
isTextInput={false}
isDialog={false}
onInput={setColor}
position="responsive"
isAlpha={false}
sliderDirection="horizontal"
/>
</div>
{/if}
<div class="space"></div>
{#if $darkModeStore}
<div class="field no-margin">
@@ -396,12 +428,38 @@
</div>
{#if !Capacitor.isNativePlatform()}
<div class="space"></div>
<div class="settings">
<h6>{$_('layout.bookmarklet')}</h6>
<button
class="no-margin"
onclick={async () => await Clipboard.write({ string: bookmarkletSaveToUrl() })}
>{$_('copyUrl')}</button
onclick={async () => {
await Clipboard.write({ string: bookmarkletSaveToUrl() });
addToast({
data: {
text: $_('player.share.copiedSuccess')
}
});
}}>{$_('copyUrl')}</button
>
</div>
{/if}
<style>
.color-picker {
--cp-bg-color: var(--surface-container);
--cp-border-color: transparent;
--cp-text-color: var(--on-surface);
--cp-input-color: var(--surface);
--cp-button-hover-color: var(--surface-variant);
--slider-width: 50px;
--picker-width: 45vw;
width: 100%;
}
@media screen and (max-width: 640px) {
.color-picker {
--picker-width: 85vw;
}
}
</style>
+26 -1
View File
@@ -19,7 +19,7 @@ body {
overflow: hidden;
}
:has(>main) {
:has(> main) {
height: 100%;
min-height: 0;
overflow: hidden;
@@ -96,6 +96,31 @@ menu {
right: 0;
}
.color-picker .picker {
display: none !important;
}
.color-picker .wrapper {
padding: 0 !important;
margin: 0 !important;
}
.color-picker .thumb {
height: 20px !important;
width: 20px !important;
opacity: 0.5 !important;
}
.color-picker .track {
border-radius: 0.25rem !important;
}
.color-picker .h {
margin-top: 0 !important;
justify-content: flex-start !important;
margin: 0 !important;
}
@media screen and (max-width: 1000px) {
menu.mobile {
position: fixed !important;
+100 -78
View File
@@ -1,4 +1,7 @@
import { page } from '$app/state';
import { get, type Writable } from 'svelte/store';
import { z } from 'zod';
import {
darkModeStore,
deArrowEnabledStore,
@@ -34,256 +37,275 @@ import {
interfaceAutoExpandChapters,
playerDefaultPlaybackSpeed,
playerCCByDefault,
playerMiniplayerEnabled
playerMiniplayerEnabled,
sponsorBlockCategoriesStore
} from '$lib/store';
import { get, type Writable } from 'svelte/store';
const persistedStores: {
type PersistedStore<T> = {
name: string;
store: Writable<any>;
type: 'string' | 'boolean' | 'array' | 'number';
}[] = [
store: Writable<T>;
schema: z.ZodType<T>;
serialize?: (value: T) => string;
};
function parseWithSchema<T>(schema: z.ZodType<T>, raw: unknown): T | undefined {
try {
if (typeof raw === 'string') {
// Try JSON first (records / objects)
try {
return schema.parse(JSON.parse(raw));
} catch {
return schema.parse(raw);
}
}
return schema.parse(raw);
} catch {
return undefined;
}
}
const zBoolean = z.coerce.boolean();
const zNumber = z.coerce.number();
const zString = z.string();
const zChapterMode = z.enum(['automatic', 'manual', 'timeline']);
const zChapterModeRecord = z.record(z.string(), zChapterMode.optional());
const persistedStores: PersistedStore<any>[] = [
{
name: 'returnYTDislikesInstance',
store: returnYTDislikesInstanceStore,
type: 'string'
schema: zString
},
{
name: 'darkMode',
store: darkModeStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'themeColor',
store: themeColorStore,
type: 'string'
schema: zString
},
{
name: 'autoPlay',
store: playerAutoPlayStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'alwaysLoop',
store: playerAlwaysLoopStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'proxyVideos',
store: playerProxyVideosStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'savePlaybackPosition',
store: playerSavePlaybackPositionStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'theatreModeByDefault',
store: playerTheatreModeByDefaultStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'autoplayNextByDefault',
store: playerAutoplayNextByDefaultStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'returnYtDislikes',
store: returnYtDislikesStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'searchSuggestions',
store: interfaceSearchSuggestionsStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'sponsorBlock',
store: sponsorBlockStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'sponsorBlockUrl',
store: sponsorBlockUrlStore,
type: 'string'
schema: zString
},
{
name: 'deArrowInstance',
store: deArrowInstanceStore,
type: 'string'
schema: zString
},
{
name: 'deArrowEnabled',
store: deArrowEnabledStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'deArrowThumbnailInstance',
store: deArrowThumbnailInstanceStore,
type: 'string'
schema: zString
},
{
name: 'syncious',
store: synciousStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'synciousInstance',
store: synciousInstanceStore,
type: 'string'
schema: zString
},
{
name: 'region',
store: interfaceRegionStore,
type: 'string'
schema: zString
},
{
name: 'forceCase',
store: interfaceForceCase,
type: 'string'
schema: zString
},
{
name: 'autoExpandComments',
store: interfaceAutoExpandComments,
type: 'boolean'
schema: zBoolean
},
{
name: 'autoExpandDesc',
store: interfaceAutoExpandDesc,
type: 'boolean'
schema: zBoolean
},
{
name: 'deArrowTitlesOnly',
store: deArrowTitlesOnly,
type: 'boolean'
schema: zBoolean
},
{
name: 'sponsorBlockDisplayToast',
store: sponsorBlockDisplayToastStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'amoledTheme',
store: interfaceAmoledTheme,
type: 'boolean'
schema: zBoolean
},
{
name: 'showWarning',
store: showWarningStore,
type: 'boolean'
schema: zBoolean
},
{
name: 'lowBandwidthMode',
store: interfaceLowBandwidthMode,
type: 'boolean'
schema: zBoolean
},
{
name: 'displayThumbnailAvatars',
store: interfaceDisplayThumbnailAvatars,
type: 'boolean'
schema: zBoolean
},
{
name: 'defaultLanguage',
store: playerDefaultLanguage,
type: 'boolean'
schema: zString
},
{
name: 'defaultPage',
store: interfaceDefaultPage,
type: 'string'
schema: zString
},
{
name: 'defaultQuality',
store: playerDefaultQualityStore,
type: 'string'
schema: zString
},
{
name: 'autoExpandChapters',
store: interfaceAutoExpandChapters,
type: 'boolean'
schema: zBoolean
},
{
name: 'defaultPlaybackSpeed',
store: playerDefaultPlaybackSpeed,
type: 'number'
schema: zNumber
},
{
name: 'CCByDefault',
store: playerCCByDefault,
type: 'boolean'
schema: zBoolean
},
{
name: 'miniplayerEnabled',
store: playerMiniplayerEnabled,
type: 'boolean'
schema: zBoolean
},
{
name: 'sponsorBlockCategoriesv2',
store: sponsorBlockCategoriesStore,
schema: zChapterModeRecord,
serialize: JSON.stringify
}
];
function setStores(toSet: Record<string, any>, overwriteExisting: boolean = false) {
persistedStores.forEach((store) => {
const paramValue = toSet[store.name];
if (typeof paramValue !== 'undefined' && overwriteExisting) {
let value: any;
function setStores(toSet: Record<string, unknown>, overwriteExisting = false) {
if (!overwriteExisting) return;
if (store.type === 'array') {
value = paramValue.split(',');
} else if (store.type === 'number') {
value = Number(paramValue);
} else if (store.type === 'boolean') {
value = typeof paramValue === 'string' ? paramValue === 'true' : paramValue;
} else {
value = paramValue;
}
for (const { name, store, schema } of persistedStores) {
const raw = toSet[name];
if (raw === undefined) continue;
store.store.set(value);
const parsed = parseWithSchema(schema, raw);
if (parsed !== undefined) {
store.set(parsed);
}
});
}
}
export function loadSettingsFromEnv() {
if (typeof import.meta.env.VITE_DEFAULT_SETTINGS !== 'string') return;
// If $VITE_DEFAULT_SETTINGS is passed via docker, it will be wrapped with quotations.
let defaultSettingsJson = import.meta.env.VITE_DEFAULT_SETTINGS;
let raw = import.meta.env.VITE_DEFAULT_SETTINGS;
if (defaultSettingsJson.startsWith('"')) {
defaultSettingsJson = defaultSettingsJson.slice(1);
}
if (defaultSettingsJson.endsWith('"')) {
defaultSettingsJson = defaultSettingsJson.slice(0, -1);
}
// Docker wraps env vars in quotes
if (raw.startsWith('"')) raw = raw.slice(1);
if (raw.endsWith('"')) raw = raw.slice(0, -1);
let isInitialLoad = false;
try {
const isInitialLoadStorage = localStorage.getItem('initialLoadState');
if (isInitialLoadStorage === null) {
if (localStorage.getItem('initialLoadState') === null) {
isInitialLoad = true;
localStorage.setItem('initialLoadState', '0');
}
} catch {
// In an environment where localstorage isn't allowed, treat as initial load.
isInitialLoad = true;
}
try {
const defaultSettings = JSON.parse(defaultSettingsJson);
setStores(defaultSettings, isInitialLoad);
} catch (error) {
console.log(error);
const parsed = JSON.parse(raw);
setStores(parsed, isInitialLoad);
} catch (err) {
console.error(err);
}
}
export function bookmarkletSaveToUrl(): string {
const url = new URL(location.origin);
persistedStores.forEach((store) => {
const value = get(store.store);
if (value !== null && value !== undefined) {
url.searchParams.set(store.name, value.toString());
for (const { name, store, serialize } of persistedStores) {
const value = get(store);
const encoded = serialize ? serialize(value) : value?.toString();
if (encoded !== undefined) {
url.searchParams.set(name, encoded);
}
});
}
return url.toString();
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.13.13"
LATEST_VERSION = "1.13.14"
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")