+35
-10
@@ -59,14 +59,14 @@ server {
|
||||
```
|
||||
### Nginx Proxy Manager example
|
||||
1. Tab: Details - Create a new proxy host with SSL on (Let's Encrypt or your own certificate).
|
||||
2. Tab: Custom locations, fill in IP and port. Click gear icon to add some security headers.
|
||||
2. Tab: Custom locations, fill in IP and port. Click gear icon to add some security headers.
|
||||
|
||||
Click image for fullsize:
|
||||
Click image for fullsize:
|
||||
<img src="https://github.com/user-attachments/assets/bd7be837-5f08-4ceb-9fdf-f0f445228075" width=15% height=15%>
|
||||
|
||||
|
||||
|
||||
**Add:**
|
||||
**Add:**
|
||||
```nginx
|
||||
if ($request_method = OPTIONS) {
|
||||
return 204;
|
||||
@@ -97,12 +97,37 @@ http:
|
||||
- PATCH
|
||||
- PUT
|
||||
- DELETE
|
||||
accessControlAllowHeaders:
|
||||
accessControlAllowHeaders:
|
||||
- User-Agent
|
||||
- Authorization
|
||||
- Authorization
|
||||
- Content-Type
|
||||
```
|
||||
|
||||
### Invidious Companion support
|
||||
The `VITE_DEFAULT_COMPANION_INSTANCE` environment variable allows you to specify a custom [Invidious Companion](https://github.com/iv-org/invidious-companion) instance.
|
||||
|
||||
To use this with Materialious, your Invidious Companion instance must be accessible with proper CORS headers. Fortunately, you can reuse the same reverse proxy configuration (with CORS modifications) that you applied to your Invidious instance—just apply it to your companion domain as well.
|
||||
|
||||
For example, if you're using Caddy, you can configure your companion domain like this:
|
||||
|
||||
```caddy
|
||||
companion.example.com {
|
||||
@cors_preflight {
|
||||
method OPTIONS
|
||||
}
|
||||
respond @cors_preflight 204
|
||||
|
||||
header Access-Control-Allow-Credentials true
|
||||
header Access-Control-Allow-Origin "https://materialious.example.com" {
|
||||
defer
|
||||
}
|
||||
header Access-Control-Allow-Methods "GET, POST, OPTIONS, HEAD, PATCH, PUT, DELETE"
|
||||
header Access-Control-Allow-Headers "User-Agent, Authorization, Content-Type"
|
||||
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
```
|
||||
|
||||
### Other
|
||||
Please open a PR request or issue if you implement this in a different reverse proxy.
|
||||
|
||||
@@ -132,10 +157,13 @@ services:
|
||||
# URL to your proxied Invidious instance
|
||||
VITE_DEFAULT_INVIDIOUS_INSTANCE: "https://invidious.materialio.us"
|
||||
|
||||
# URL to your proxied Companion instance
|
||||
VITE_DEFAULT_COMPANION_INSTANCE: "https://companion.materialio.us"
|
||||
|
||||
# URL TO RYD
|
||||
# Leave blank to disable completely.
|
||||
VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE: "https://returnyoutubedislikeapi.com"
|
||||
|
||||
|
||||
# URL to Sponsorblock
|
||||
# Leave blank to completely disable sponsorblock.
|
||||
VITE_DEFAULT_SPONSERBLOCK_INSTANCE: "https://sponsor.ajay.app"
|
||||
@@ -148,9 +176,6 @@ services:
|
||||
|
||||
# Look at "Overwriting Materialious defaults" for all the accepted values.
|
||||
VITE_DEFAULT_SETTINGS: '{"themeColor": "#2596be","region": "US"}'
|
||||
|
||||
# Set the default video/audio bitrate to use, -1 automatic, 999999 always best quality
|
||||
VITE_DEFAULT_DASH_BITRATE: -1
|
||||
```
|
||||
|
||||
### Overwriting Materialious defaults
|
||||
@@ -270,4 +295,4 @@ Add these additional environment variables to Materialious.
|
||||
VITE_DEFAULT_PEERJS_HOST: "peerjs.example.com"
|
||||
VITE_DEFAULT_PEERJS_PATH: "/"
|
||||
VITE_DEFAULT_PEERJS_PORT: 443
|
||||
```
|
||||
```
|
||||
|
||||
@@ -11,6 +11,7 @@ COPY . .
|
||||
|
||||
# Create placeholder env vars
|
||||
RUN echo "VITE_DEFAULT_INVIDIOUS_INSTANCE=VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEHOLDER" > .env && \
|
||||
echo "VITE_DEFAULT_COMPANION_INSTANCE=VITE_DEFAULT_COMPANION_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_SPONSERBLOCK_INSTANCE=VITE_DEFAULT_SPONSERBLOCK_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_DEARROW_INSTANCE=VITE_DEFAULT_DEARROW_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
echo "VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE=VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE_PLACEHOLDER" >> .env && \
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "us.materialio.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 116
|
||||
versionName "1.8.12"
|
||||
versionCode 117
|
||||
versionName "1.9.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -61,7 +61,11 @@
|
||||
|
||||
|
||||
|
||||
<release version="1.8.12" date="2025-6-04">
|
||||
|
||||
<release version="1.9.0" date="2025-6-04">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.9.0</url>
|
||||
</release>
|
||||
<release version="1.8.12" date="2025-6-04">
|
||||
<url>https://github.com/Materialious/Materialious/releases/tag/1.8.12</url>
|
||||
</release>
|
||||
<release version="1.8.11" date="2025-6-03">
|
||||
|
||||
Generated
+9
-6
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.8.10",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.8.10",
|
||||
"version": "1.9.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz",
|
||||
"chokidar": "~4.0.3",
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~1.1.0",
|
||||
@@ -1799,10 +1799,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/capacitor-nodejs": {
|
||||
"version": "1.0.0-beta.7",
|
||||
"resolved": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"integrity": "sha512-7NwSMxE06nqpuFKyQlt3mWo+VG6Od0d6PBxPKVyPmaptE6sSoimzU/uzQXPWoHV5bwbjgcF+R0BSJAZAyy4gsg==",
|
||||
"version": "1.0.0-beta.8",
|
||||
"resolved": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz",
|
||||
"integrity": "sha512-VAMCTBqz+sA3762kFhSyzXS0unm0EpsVSHLmDYsTYPTiXXZaFcGk27vfXakRyyXJexAUG2Y5rZka5BvH6EmeZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.8.12",
|
||||
"version": "1.9.0",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
@@ -24,7 +24,7 @@
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz",
|
||||
"chokidar": "~4.0.3",
|
||||
"electron-is-dev": "~2.0.0",
|
||||
"electron-serve": "~1.1.0",
|
||||
@@ -44,4 +44,4 @@
|
||||
"capacitor",
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,13 @@ import {
|
||||
setupElectronDeepLinking
|
||||
} from '@capacitor-community/electron';
|
||||
import type { MenuItemConstructorOptions } from 'electron';
|
||||
import { app, ipcMain, MenuItem } from 'electron';
|
||||
import { app, ipcMain, MenuItem, session, webContents } from 'electron';
|
||||
import electronIsDev from 'electron-is-dev';
|
||||
import unhandled from 'electron-unhandled';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
import BG, {
|
||||
buildURL,
|
||||
DescrambledChallenge,
|
||||
GOOG_API_KEY,
|
||||
USER_AGENT,
|
||||
WebPoSignalOutput
|
||||
} from 'bgutils-js';
|
||||
import BG, { buildURL, GOOG_API_KEY, USER_AGENT, WebPoSignalOutput } from 'bgutils-js';
|
||||
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
|
||||
|
||||
// Graceful handling of unhandled errors.
|
||||
@@ -54,6 +48,8 @@ if (electronIsDev) {
|
||||
setupReloadWatcher(myCapacitorApp);
|
||||
}
|
||||
|
||||
let allowInsecureSSL = false;
|
||||
|
||||
// Run Application
|
||||
(async () => {
|
||||
// Wait for electron app to be ready.
|
||||
@@ -66,6 +62,15 @@ if (electronIsDev) {
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
})();
|
||||
|
||||
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
|
||||
if (allowInsecureSSL) {
|
||||
event.preventDefault(); // prevent default behavior (blocking the cert)
|
||||
callback(true); // trust the certificate
|
||||
} else {
|
||||
callback(false); // reject others
|
||||
}
|
||||
});
|
||||
|
||||
// Handle when all of our windows are close (platforms have their own expectations).
|
||||
app.on('window-all-closed', function () {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
@@ -154,3 +159,11 @@ ipcMain.handle(
|
||||
return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData));
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle('setAllowInsecureSSL', async (_, allow) => {
|
||||
allowInsecureSSL = allow;
|
||||
await session.defaultSession.clearAuthCache();
|
||||
await session.defaultSession.clearCache();
|
||||
|
||||
return allowInsecureSSL;
|
||||
});
|
||||
|
||||
@@ -5,5 +5,8 @@ require('./rt/electron-rt');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
generatePoToken: (bgChallenge: DescrambledChallenge, requestKey: string, visitorData: string) =>
|
||||
ipcRenderer.invoke('generatePoToken', bgChallenge, requestKey, visitorData)
|
||||
ipcRenderer.invoke('generatePoToken', bgChallenge, requestKey, visitorData),
|
||||
setAllowInsecureSSL: async (allow: boolean) => {
|
||||
return await ipcRenderer.invoke('setAllowInsecureSSL', allow);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.8.12",
|
||||
"version": "1.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -3,6 +3,7 @@ ROOT_DIR=/usr/share/nginx/html
|
||||
|
||||
# Replace env vars in JavaScript and HTML files served by NGINX
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_INVIDIOUS_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_INVIDIOUS_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_COMPANION_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_COMPANION_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_RETURNYTDISLIKES_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_SPONSERBLOCK_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_SPONSERBLOCK_INSTANCE"'|g' {} +
|
||||
find "$ROOT_DIR" -type f \( -name "*.js" -o -name "*.html" \) -exec sed -i 's|VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE_PLACEHOLDER|'"$VITE_DEFAULT_DEARROW_THUMBNAIL_INSTANCE"'|g' {} +
|
||||
|
||||
Vendored
+1
@@ -18,6 +18,7 @@ declare global {
|
||||
requestKey: string,
|
||||
visitorData: string
|
||||
) => Promise<string>;
|
||||
setAllowInsecureSSL: (allowInsecureSSL: boolean) => Promoise<boolean>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { interfaceAllowInsecureRequests } from '$lib/store';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
@@ -53,14 +52,5 @@ if (Capacitor.getPlatform() === 'android') {
|
||||
return originalXhrOpen.apply(this, args);
|
||||
};
|
||||
|
||||
// Used to toggle rejectUnauthorized in the backend
|
||||
interfaceAllowInsecureRequests.subscribe(async (isAllowed) => {
|
||||
if (isAllowed) {
|
||||
await fetch('http://materialious__allow-insecure-requests');
|
||||
} else {
|
||||
await fetch('http://materialious__deny-insecure-requests');
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => goto('/', { replaceState: true }), 100);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Channel } from '../api/model';
|
||||
import { truncate } from '../misc';
|
||||
import { insecureRequestImageHandler, truncate } from '../misc';
|
||||
import { interfaceLowBandwidthMode } from '../store';
|
||||
|
||||
interface Props {
|
||||
@@ -19,9 +19,9 @@
|
||||
onMount(async () => {
|
||||
if (get(interfaceLowBandwidthMode)) return;
|
||||
|
||||
const img = new Image();
|
||||
img.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails));
|
||||
|
||||
const img = await insecureRequestImageHandler(
|
||||
proxyGoogleImage(getBestThumbnail(channel.authorThumbnails))
|
||||
);
|
||||
img.onload = () => {
|
||||
channelPfp = img;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { numberWithCommas } from '$lib/numbers';
|
||||
import { interfaceLowBandwidthMode } from '$lib/store';
|
||||
import { onMount } from 'svelte';
|
||||
import CommentSelf from './Comment.svelte';
|
||||
import { insecureRequestImageHandler } from '$lib/misc';
|
||||
|
||||
interface Props {
|
||||
comment: Comment;
|
||||
@@ -36,16 +38,28 @@
|
||||
|
||||
return processedHtml;
|
||||
}
|
||||
|
||||
let userPfp = $state('');
|
||||
onMount(async () => {
|
||||
if ($interfaceLowBandwidthMode) return;
|
||||
const img = await insecureRequestImageHandler(
|
||||
proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))
|
||||
);
|
||||
|
||||
img.onload = () => {
|
||||
userPfp = img.src;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="comment">
|
||||
{#if !$interfaceLowBandwidthMode}
|
||||
<div class="comment-header">
|
||||
<img
|
||||
class="circle small"
|
||||
src={proxyGoogleImage(getBestThumbnail(comment.authorThumbnails))}
|
||||
alt="comment profile"
|
||||
/>
|
||||
{#if userPfp}
|
||||
<img class="circle small" src={userPfp} alt="comment profile" />
|
||||
{:else}
|
||||
<progress class="circle"></progress>
|
||||
{/if}
|
||||
<div class="comment-info">
|
||||
<a href={`/channel/${comment.authorId}`} class="author">
|
||||
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}>
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
}
|
||||
|
||||
let { hashtag }: Props = $props();
|
||||
|
||||
let channelPfp: HTMLImageElement | undefined;
|
||||
</script>
|
||||
|
||||
<a href={hashtag.url} class="wave" style="min-width: 100%;min-height: 100%;">
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import type { VideoPlay } from '../api/model';
|
||||
import {
|
||||
authStore,
|
||||
companionStore,
|
||||
instanceStore,
|
||||
playerAndroidLockOrientation,
|
||||
playerAutoPlayStore,
|
||||
@@ -76,7 +77,6 @@
|
||||
const sessionId = Array.from(Array(16), () => Math.floor(Math.random() * 36).toString(36)).join(
|
||||
''
|
||||
);
|
||||
let isLive = false;
|
||||
let videoPlaybackUstreamerConfig: string | undefined;
|
||||
let serverAbrStreamingUrl: URL | undefined = undefined;
|
||||
let drmParams: string | undefined;
|
||||
@@ -251,7 +251,7 @@
|
||||
'language',
|
||||
'statistics'
|
||||
],
|
||||
playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3],
|
||||
playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3],
|
||||
enableTooltips: false,
|
||||
seekBarColors: {
|
||||
played: (await getDynamicTheme())['--primary']
|
||||
@@ -259,8 +259,6 @@
|
||||
});
|
||||
|
||||
if (data.video.fallbackPatch === 'youtubejs' && data.video.ytjs) {
|
||||
isLive = !!data.video.ytjs.video.basic_info.is_live;
|
||||
|
||||
if (
|
||||
data.video.ytjs.rawApiResponse.data.streamingData &&
|
||||
(data.video.ytjs.rawApiResponse.data.streamingData as any).drmParams
|
||||
@@ -343,7 +341,7 @@
|
||||
type === shaka.net.NetworkingEngine.RequestType.SEGMENT &&
|
||||
url.pathname.includes('videoplayback')
|
||||
) {
|
||||
if (!isLive) {
|
||||
if (!data.video.liveNow) {
|
||||
const currentFormat = formatList.find(
|
||||
(format) =>
|
||||
fromFormat(format) === (new URL(request.uris[0]).searchParams.get('___key') || '')
|
||||
@@ -632,7 +630,12 @@
|
||||
await player.attach(playerElement);
|
||||
|
||||
if (!data.video.hlsUrl) {
|
||||
let dashUrl = data.video.dashUrl;
|
||||
let dashUrl: string = '';
|
||||
if ($companionStore) {
|
||||
dashUrl = `${$companionStore}/api/manifest/dash/id/${data.video.videoId}`;
|
||||
} else {
|
||||
dashUrl = data.video.dashUrl;
|
||||
}
|
||||
|
||||
if (!data.video.fallbackPatch && (!Capacitor.isNativePlatform() || proxyVideos)) {
|
||||
dashUrl += '?local=true';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Playlist } from '../api/model';
|
||||
import { truncate } from '../misc';
|
||||
import { insecureRequestImageHandler, truncate } from '../misc';
|
||||
import { interfaceLowBandwidthMode } from '../store';
|
||||
|
||||
interface Props {
|
||||
@@ -21,20 +21,23 @@
|
||||
|
||||
const playlistLink = `/playlist/${playlist.playlistId}`;
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
if (get(interfaceLowBandwidthMode)) return;
|
||||
|
||||
img = new Image();
|
||||
let imgSrc = '';
|
||||
|
||||
if (playlist.videos.length > 0) {
|
||||
img.src = getBestThumbnail(playlist.videos[0].videoThumbnails) || '';
|
||||
imgSrc = getBestThumbnail(playlist.videos[0].videoThumbnails) || '';
|
||||
} else if (playlist.playlistThumbnail) {
|
||||
img.src = playlist.playlistThumbnail;
|
||||
imgSrc = playlist.playlistThumbnail;
|
||||
} else {
|
||||
img.src = '';
|
||||
imgSrc = '';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
img = await insecureRequestImageHandler(imgSrc);
|
||||
|
||||
img.onload = () => {
|
||||
loading = false;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { getPages } from '../../navPages';
|
||||
import {
|
||||
authStore,
|
||||
companionStore,
|
||||
darkModeStore,
|
||||
instanceStore,
|
||||
interfaceAllowInsecureRequests,
|
||||
@@ -20,7 +21,6 @@
|
||||
interfaceAutoExpandComments,
|
||||
interfaceAutoExpandDesc,
|
||||
interfaceDefaultPage,
|
||||
interfaceDisplayThumbnailAvatars,
|
||||
interfaceForceCase,
|
||||
interfaceLowBandwidthMode,
|
||||
interfaceRegionStore,
|
||||
@@ -31,6 +31,7 @@
|
||||
} from '../../store';
|
||||
|
||||
let invidiousInstance = $state(get(instanceStore));
|
||||
let companionInstance = $state(get(companionStore));
|
||||
let region = $state(get(interfaceRegionStore));
|
||||
let forceCase = $state(get(interfaceForceCase));
|
||||
let defaultPage = $state(get(interfaceDefaultPage));
|
||||
@@ -92,10 +93,28 @@
|
||||
ui('#dialog-settings');
|
||||
}
|
||||
|
||||
function setCompanion(event: Event) {
|
||||
event.preventDefault();
|
||||
companionStore.set(ensureNoTrailingSlash(companionInstance));
|
||||
}
|
||||
|
||||
function allowInsecureRequests() {
|
||||
if (Capacitor.getPlatform() !== 'android') return;
|
||||
if (!Capacitor.isNativePlatform()) return;
|
||||
interfaceAllowInsecureRequests.set(!$interfaceAllowInsecureRequests);
|
||||
}
|
||||
|
||||
// Used to toggle rejectUnauthorized in the backend
|
||||
interfaceAllowInsecureRequests.subscribe(async (isAllowed) => {
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
if (isAllowed) {
|
||||
await fetch('http://materialious__allow-insecure-requests');
|
||||
} else {
|
||||
await fetch('http://materialious__deny-insecure-requests');
|
||||
}
|
||||
} else if (Capacitor.getPlatform() === 'electron') {
|
||||
await window.electronAPI.setAllowInsecureSSL(isAllowed);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if Capacitor.isNativePlatform()}
|
||||
@@ -116,24 +135,39 @@
|
||||
{#if invalidInstance}
|
||||
<div style="margin-bottom: 6em;"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if Capacitor.getPlatform() === 'android' && (invalidInstance || $interfaceAllowInsecureRequests)}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.allowInsecureRequests')}</div>
|
||||
{#if Capacitor.isNativePlatform() && (invalidInstance || $interfaceAllowInsecureRequests)}
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.allowInsecureRequests')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceAllowInsecureRequests}
|
||||
onclick={allowInsecureRequests}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={setCompanion}>
|
||||
<nav>
|
||||
<div class="field label border max">
|
||||
<input bind:value={companionInstance} name="companion-instance" type="text" />
|
||||
<label for="companion-instance">{$_('layout.companionUrl')}</label>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceAllowInsecureRequests}
|
||||
onclick={allowInsecureRequests}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
<button class="square round">
|
||||
<i>done</i>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</form>
|
||||
{#if invalidInstance}
|
||||
<div style="margin-bottom: 6em;"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<button onclick={toggleDarkMode} class="no-margin">
|
||||
@@ -171,22 +205,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
<div>{$_('layout.displayThumbnailAvatars')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$interfaceDisplayThumbnailAvatars}
|
||||
onclick={() => interfaceDisplayThumbnailAvatars.set(!$interfaceDisplayThumbnailAvatars)}
|
||||
/>
|
||||
<span></span>
|
||||
</label>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="field no-margin">
|
||||
<nav class="no-padding">
|
||||
<div class="max">
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
|
||||
import { getBestThumbnail } from '$lib/images';
|
||||
import { letterCase } from '$lib/letterCasing';
|
||||
import { cleanNumber, videoLength } from '$lib/numbers';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { getChannel, getDeArrow, getThumbnail, getVideoProgress } from '../api';
|
||||
import { getDeArrow, getThumbnail, getVideoProgress } from '../api';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model';
|
||||
import { truncate } from '../misc';
|
||||
import { insecureRequestImageHandler, truncate } from '../misc';
|
||||
import type { PlayerEvents } from '../player';
|
||||
import {
|
||||
authStore,
|
||||
deArrowEnabledStore,
|
||||
interfaceDisplayThumbnailAvatars,
|
||||
interfaceLowBandwidthMode,
|
||||
playerSavePlaybackPositionStore,
|
||||
syncPartyConnectionsStore,
|
||||
@@ -29,8 +28,6 @@
|
||||
|
||||
let { video = $bindable(), playlistId = '', sideways = $bindable(false) }: Props = $props();
|
||||
|
||||
let authorImg: HTMLImageElement | undefined = $state();
|
||||
|
||||
let placeholderHeight: number = $state(0);
|
||||
|
||||
let watchUrl = new URL(`${location.origin}/watch/${video.videoId}`);
|
||||
@@ -64,24 +61,9 @@
|
||||
|
||||
if (window.innerWidth <= 1500) {
|
||||
sideways = false;
|
||||
if (typeof authorImg === 'undefined') loadAuthor();
|
||||
} else sideways = true;
|
||||
}
|
||||
|
||||
async function loadAuthor() {
|
||||
if (!get(interfaceDisplayThumbnailAvatars)) return;
|
||||
|
||||
try {
|
||||
const channel = await getChannel(video.authorId, { priority: 'low' });
|
||||
const loadedPfp = new Image();
|
||||
loadedPfp.src = proxyGoogleImage(getBestThumbnail(channel.authorThumbnails, 75, 75));
|
||||
|
||||
loadedPfp.onload = () => {
|
||||
authorImg = loadedPfp;
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
calcThumbnailPlaceholderHeight();
|
||||
|
||||
@@ -95,16 +77,9 @@
|
||||
|
||||
if (get(interfaceLowBandwidthMode)) return;
|
||||
|
||||
// Load author details in background.
|
||||
if (!sideways) {
|
||||
loadAuthor();
|
||||
}
|
||||
|
||||
let imageSrc = getBestThumbnail(video.videoThumbnails) as string;
|
||||
|
||||
if (get(deArrowEnabledStore)) {
|
||||
let locatedThumbnail = false;
|
||||
|
||||
try {
|
||||
const deArrow = await getDeArrow(video.videoId);
|
||||
for (const title of deArrow.titles) {
|
||||
@@ -119,16 +94,13 @@
|
||||
if (thumbnail.timestamp !== null) {
|
||||
imageSrc = await getThumbnail(video.videoId, thumbnail.timestamp);
|
||||
}
|
||||
locatedThumbnail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.src = imageSrc;
|
||||
|
||||
const img = await insecureRequestImageHandler(imageSrc);
|
||||
img.onload = () => {
|
||||
thumbnail = img;
|
||||
};
|
||||
@@ -225,15 +197,6 @@
|
||||
</div>
|
||||
|
||||
<div class="thumbnail-details video-title">
|
||||
{#if !sideways && !$interfaceLowBandwidthMode && $interfaceDisplayThumbnailAvatars}
|
||||
<div style="margin-right: 1em;">
|
||||
{#if authorImg}
|
||||
<img src={authorImg.src} alt="Author" class="circle small" />
|
||||
{:else}
|
||||
<progress style="padding: 15px;" class="circle small"></progress>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="video-title">
|
||||
<a
|
||||
style="padding-left: 1px;"
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
"lowBandwidthMode": "Low bandwidth mode",
|
||||
"displayThumbnailAvatars": "Thumbnail avatars (Slow)",
|
||||
"searchHistory": "Search history",
|
||||
"companionUrl": "Companion URL",
|
||||
"theme": {
|
||||
"theme": "Theme",
|
||||
"darkMode": "Dark mode",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { page } from '$app/stores';
|
||||
import he from 'he';
|
||||
import type Peer from 'peerjs';
|
||||
import { get } from 'svelte/store';
|
||||
import { instanceStore } from './store';
|
||||
import { instanceStore, interfaceAllowInsecureRequests } from './store';
|
||||
|
||||
export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
@@ -16,10 +16,7 @@ export function decodeHtmlCharCodes(str: string): string {
|
||||
|
||||
export function proxyVideoUrl(source: string): string {
|
||||
const rawSrc = new URL(source);
|
||||
rawSrc.host = get(instanceStore).replace('http://', '').replace(
|
||||
'https://',
|
||||
''
|
||||
);
|
||||
rawSrc.host = get(instanceStore).replace('http://', '').replace('https://', '');
|
||||
|
||||
return rawSrc.toString();
|
||||
}
|
||||
@@ -46,14 +43,11 @@ export async function peerJs(peerId: string): Promise<Peer> {
|
||||
if (typeof PeerInstance === 'undefined') {
|
||||
PeerInstance = (await import('peerjs')).Peer;
|
||||
}
|
||||
return new PeerInstance(
|
||||
peerId,
|
||||
{
|
||||
host: import.meta.env.VITE_DEFAULT_PEERJS_HOST || '0.peerjs.com',
|
||||
path: import.meta.env.VITE_DEFAULT_PEERJS_PATH || '/',
|
||||
port: import.meta.env.VITE_DEFAULT_PEERJS_PORT || 443
|
||||
}
|
||||
);
|
||||
return new PeerInstance(peerId, {
|
||||
host: import.meta.env.VITE_DEFAULT_PEERJS_HOST || '0.peerjs.com',
|
||||
path: import.meta.env.VITE_DEFAULT_PEERJS_PATH || '/',
|
||||
port: import.meta.env.VITE_DEFAULT_PEERJS_PORT || 443
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureNoTrailingSlash(url: any): string {
|
||||
@@ -61,3 +55,19 @@ export function ensureNoTrailingSlash(url: any): string {
|
||||
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
}
|
||||
|
||||
export async function insecureRequestImageHandler(source: string): Promise<HTMLImageElement> {
|
||||
const img = new Image();
|
||||
if (get(interfaceAllowInsecureRequests)) {
|
||||
const imgResp = await fetch(source);
|
||||
if (!imgResp.ok) {
|
||||
img.src = '';
|
||||
}
|
||||
|
||||
img.src = URL.createObjectURL(await imgResp.blob());
|
||||
} else {
|
||||
img.src = source;
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { persisted } from 'svelte-persisted-store';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { TitleCase } from './letterCasing';
|
||||
import type { Channel, HashTag, Playlist, PlaylistPageVideo, Video, VideoBase } from './api/model';
|
||||
import { ensureNoTrailingSlash } from './misc';
|
||||
|
||||
function platformDependentDefault(givenValue: any, defaultValue: any): any {
|
||||
if (typeof givenValue !== 'undefined' && typeof givenValue !== null) {
|
||||
@@ -17,11 +18,19 @@ function platformDependentDefault(givenValue: any, defaultValue: any): any {
|
||||
export const instanceStore: Writable<string> = persisted(
|
||||
'invidiousInstance',
|
||||
platformDependentDefault(
|
||||
import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE,
|
||||
ensureNoTrailingSlash(import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE),
|
||||
'https://invidious.materialio.us'
|
||||
)
|
||||
);
|
||||
|
||||
export const companionStore: Writable<string | undefined> = persisted(
|
||||
'companionInstance',
|
||||
platformDependentDefault(
|
||||
ensureNoTrailingSlash(import.meta.env.VITE_DEFAULT_COMPANION_INSTANCE),
|
||||
undefined
|
||||
)
|
||||
);
|
||||
|
||||
export const authStore: Writable<null | { username: string; token: string }> = persisted(
|
||||
'authToken',
|
||||
null
|
||||
@@ -118,6 +127,6 @@ export const feedCacheStore: Writable<{
|
||||
[key: string]: (VideoBase | Video | PlaylistPageVideo)[];
|
||||
}> = writable({});
|
||||
export const searchCacheStore: Writable<{
|
||||
[searchType: string]: (Channel | Video | Playlist | HashTag)[];
|
||||
[searchTypeAndQuery: string]: (Channel | Video | Playlist | HashTag)[];
|
||||
}> = writable({});
|
||||
export const feedLastItemId: Writable<string | undefined> = writable(undefined);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
async function changeType(type: 'playlist' | 'all' | 'video' | 'channel') {
|
||||
currentType = type;
|
||||
currentPage = 1;
|
||||
searchCacheStore.set({ [type]: await getSearch(data.slug, { type: type }) });
|
||||
searchCacheStore.set({ [data.searchStoreId]: await getSearch(data.slug, { type: type }) });
|
||||
}
|
||||
|
||||
async function loadMore(event: InfiniteEvent) {
|
||||
@@ -41,7 +41,7 @@
|
||||
event.detail.complete();
|
||||
} else {
|
||||
searchCacheStore.set({
|
||||
[currentType]: [...($searchCacheStore[currentType] ?? []), ...newSearch]
|
||||
[data.searchStoreId]: [...($searchCacheStore[data.searchStoreId] ?? []), ...newSearch]
|
||||
});
|
||||
event.detail.loaded();
|
||||
}
|
||||
@@ -81,11 +81,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $searchCacheStore[currentType].length > 0}
|
||||
{#if $searchCacheStore[data.searchStoreId]}
|
||||
<div class="page right active">
|
||||
<div class="space"></div>
|
||||
<div class="grid">
|
||||
{#each $searchCacheStore[currentType] as item}
|
||||
{#each $searchCacheStore[data.searchStoreId] as item}
|
||||
<ContentColumn>
|
||||
{#key item}
|
||||
<article
|
||||
|
||||
@@ -13,24 +13,26 @@ export async function load({ params, url }) {
|
||||
type = 'all';
|
||||
}
|
||||
|
||||
const search = get(searchCacheStore).all;
|
||||
const searchStoreId = type + params.slug;
|
||||
const search = get(searchCacheStore)[searchStoreId];
|
||||
|
||||
if (!search) {
|
||||
try {
|
||||
searchCacheStore.set({ [type]: await getSearch(params.slug, { type: type }) });
|
||||
searchCacheStore.set({ [searchStoreId]: await getSearch(params.slug, { type: type }) });
|
||||
} catch (errorMessage: any) {
|
||||
error(500, errorMessage);
|
||||
}
|
||||
} else {
|
||||
getSearch(params.slug, { type: type }).then((newSearch) => {
|
||||
searchCacheStore.set({
|
||||
[type]: [...new Set([...newSearch, ...search])]
|
||||
[searchStoreId]: [...new Set([...newSearch, ...search])]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
slug: params.slug,
|
||||
searchType: type
|
||||
searchType: type,
|
||||
searchStoreId: searchStoreId
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
LATEST_VERSION = "1.8.12"
|
||||
LATEST_VERSION = "1.9.0"
|
||||
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
|
||||
|
||||
WORKING_DIR = os.path.join(
|
||||
|
||||
Reference in New Issue
Block a user