Started android tv support
This commit is contained in:
@@ -15,6 +15,9 @@ android {
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -26,7 +29,7 @@ android {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies {
|
||||
implementation project(':capacitor-screen-orientation')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':hugotomazi-capacitor-navigation-bar')
|
||||
implementation project(':capacitor-android-tv-plugin')
|
||||
implementation project(':capacitor-nodejs')
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:name=".MainActivity"
|
||||
@@ -17,6 +18,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
|
||||
@@ -20,5 +20,8 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
|
||||
include ':hugotomazi-capacitor-navigation-bar'
|
||||
project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android')
|
||||
|
||||
include ':capacitor-android-tv-plugin'
|
||||
project(':capacitor-android-tv-plugin').projectDir = new File('../node_modules/capacitor-android-tv-plugin/android')
|
||||
|
||||
include ':capacitor-nodejs'
|
||||
project(':capacitor-nodejs').projectDir = new File('../node_modules/capacitor-nodejs/android')
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'us.materialio.app',
|
||||
appName: 'Materialious',
|
||||
webDir: 'build',
|
||||
plugins: {
|
||||
CapacitorNodeJS: {
|
||||
nodeDir: 'nodejs-android',
|
||||
}
|
||||
},
|
||||
server: {
|
||||
hostname: 'www.youtube.com',
|
||||
androidScheme: 'https'
|
||||
}
|
||||
appId: 'us.materialio.app',
|
||||
appName: 'Materialious',
|
||||
webDir: 'build',
|
||||
plugins: {
|
||||
CapacitorNodeJS: {
|
||||
nodeDir: 'nodejs-android'
|
||||
}
|
||||
},
|
||||
server: {
|
||||
hostname: 'www.youtube.com',
|
||||
androidScheme: 'https'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.9.8",
|
||||
"version": "1.9.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "Materialious",
|
||||
"version": "1.9.8",
|
||||
"version": "1.9.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
|
||||
Generated
+12
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "materialious",
|
||||
"version": "1.9.8",
|
||||
"version": "1.9.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "materialious",
|
||||
"version": "1.9.8",
|
||||
"version": "1.9.9",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
@@ -21,6 +21,7 @@
|
||||
"@hugotomazi/capacitor-navigation-bar": "^4.0.1",
|
||||
"beercss": "^3.8.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-android-tv-plugin": "^0.0.2",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz",
|
||||
"fuse.js": "^7.0.0",
|
||||
"googlevideo": "^3.0.0",
|
||||
@@ -4320,6 +4321,15 @@
|
||||
"license": "CC-BY-4.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/capacitor-android-tv-plugin": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/capacitor-android-tv-plugin/-/capacitor-android-tv-plugin-0.0.2.tgz",
|
||||
"integrity": "sha512-Ww+Nx7lg7cmXk+KX8zTk3G1IMvTO4pa06m6fU9rmuw54T5wOwgz39e9BNjF9kt5Vtio7blSfwPVLHCc4CdCTTg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/capacitor-nodejs": {
|
||||
"version": "1.0.0-beta.8",
|
||||
"resolved": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"@hugotomazi/capacitor-navigation-bar": "^4.0.1",
|
||||
"beercss": "^3.8.0",
|
||||
"bgutils-js": "^3.2.0",
|
||||
"capacitor-android-tv-plugin": "^0.0.2",
|
||||
"capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.8/capacitor-nodejs.tgz",
|
||||
"fuse.js": "^7.0.0",
|
||||
"googlevideo": "^3.0.0",
|
||||
@@ -70,4 +71,4 @@
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"youtubei.js": "^14.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { NodeJS } from 'capacitor-nodejs';
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
const corsProxyUrl: string = 'http://localhost:3000/';
|
||||
@@ -52,5 +53,7 @@ if (Capacitor.getPlatform() === 'android') {
|
||||
return originalXhrOpen.apply(this, args);
|
||||
};
|
||||
|
||||
setTimeout(() => goto('/', { replaceState: true }), 100);
|
||||
NodeJS.whenReady().then(() => {
|
||||
goto('/', { replaceState: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,8 +59,13 @@ export async function fetchErrorHandle(response: Response): Promise<Response> {
|
||||
return response;
|
||||
}
|
||||
|
||||
export function buildAuthHeaders(): { headers: { Authorization: string } } {
|
||||
return { headers: { Authorization: `Bearer ${get(authStore)?.token}` } };
|
||||
export function buildAuthHeaders(): { headers: Record<string, string> } {
|
||||
const authToken = get(authStore)?.token ?? '';
|
||||
if (authToken.startsWith('SID=')) {
|
||||
return { headers: { __sid_auth: authToken } };
|
||||
} else {
|
||||
return { headers: { Authorization: `Bearer ${get(authStore)?.token}` } };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTrending(fetchOptions?: RequestInit): Promise<Video[]> {
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { AndroidTv } from 'capacitor-android-tv-plugin';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let largeCol = $state('2');
|
||||
let smallCol = $state('12');
|
||||
let mediumCol = $state('6');
|
||||
|
||||
function checkWidth() {
|
||||
if (innerWidth <= 1750) {
|
||||
async function checkWidth() {
|
||||
if (await AndroidTv.isAndroidTv()) {
|
||||
mediumCol = '3';
|
||||
} else if (innerWidth <= 1750) {
|
||||
largeCol = '4';
|
||||
} else {
|
||||
largeCol = '2';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkWidth();
|
||||
onMount(async () => {
|
||||
await checkWidth();
|
||||
|
||||
addEventListener('resize', () => checkWidth());
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="s12 m6 l{largeCol}">
|
||||
<div class="s{smallCol} m{mediumCol} l{largeCol}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
import { patchYoutubeJs } from '$lib/patches/youtubejs';
|
||||
import { playbackRates } from '$lib/const';
|
||||
import { EndTimeElement } from '$lib/shaka-elements/endTime';
|
||||
import { AndroidTv } from 'capacitor-android-tv-plugin';
|
||||
|
||||
interface Props {
|
||||
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
|
||||
@@ -67,7 +68,6 @@
|
||||
let playerPosSet = false;
|
||||
let originalOrigination: ScreenOrientationResult | undefined;
|
||||
let watchProgressTimeout: NodeJS.Timeout;
|
||||
let userWantsFullscreen = false;
|
||||
let playerElementResizeObserver: ResizeObserver | undefined;
|
||||
let showVideoRetry = $state(false);
|
||||
|
||||
@@ -188,7 +188,11 @@
|
||||
}
|
||||
|
||||
async function androidHandleRotate() {
|
||||
if (Capacitor.getPlatform() === 'android' && data.video.adaptiveFormats.length > 0) {
|
||||
if (
|
||||
Capacitor.getPlatform() === 'android' &&
|
||||
data.video.adaptiveFormats.length > 0 &&
|
||||
!(await AndroidTv.isAndroidTv())
|
||||
) {
|
||||
const videoFormats = data.video.adaptiveFormats.filter((format) =>
|
||||
format.type.startsWith('video/')
|
||||
);
|
||||
@@ -479,7 +483,6 @@
|
||||
|
||||
// Required to stop buttons from being still selected when fullscreening
|
||||
document.addEventListener('fullscreenchange', async () => {
|
||||
userWantsFullscreen = document.fullscreenElement !== null;
|
||||
const buttons = document.querySelectorAll('.shaka-controls-button-panel button');
|
||||
buttons.forEach((button) => {
|
||||
// Reset the button's focus and active states
|
||||
@@ -513,7 +516,7 @@
|
||||
playerElement.pause();
|
||||
}
|
||||
|
||||
if (!document.fullscreenElement && userWantsFullscreen) {
|
||||
if (!document.fullscreenElement) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
}
|
||||
return false;
|
||||
@@ -523,7 +526,7 @@
|
||||
if (!playerElement) return;
|
||||
playerElement.currentTime = playerElement.currentTime + 10;
|
||||
|
||||
if (!document.fullscreenElement && userWantsFullscreen) {
|
||||
if (!document.fullscreenElement) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
}
|
||||
return false;
|
||||
@@ -534,7 +537,7 @@
|
||||
|
||||
playerElement.currentTime = playerElement.currentTime - 10;
|
||||
|
||||
if (!document.fullscreenElement && userWantsFullscreen) {
|
||||
if (!document.fullscreenElement) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
}
|
||||
return false;
|
||||
@@ -557,7 +560,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!document.fullscreenElement && userWantsFullscreen) {
|
||||
if (!document.fullscreenElement) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
}
|
||||
return false;
|
||||
@@ -577,9 +580,11 @@
|
||||
|
||||
playerElement.playbackRate = playerElement.playbackRate - 0.25;
|
||||
|
||||
if (!document.fullscreenElement && userWantsFullscreen) {
|
||||
if (!document.fullscreenElement) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
Mousetrap.bind('shift+right', () => {
|
||||
@@ -587,13 +592,28 @@
|
||||
|
||||
playerElement.playbackRate = playerElement.playbackRate + 0.25;
|
||||
|
||||
if (!document.fullscreenElement && userWantsFullscreen) {
|
||||
if (!document.fullscreenElement) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
setChapterMarkers();
|
||||
|
||||
if (await AndroidTv.isAndroidTv()) {
|
||||
shakaUi.getControls()?.toggleFullScreen();
|
||||
|
||||
Mousetrap.bind('enter', () => {
|
||||
if (playerElement?.paused) {
|
||||
playerElement?.play();
|
||||
} else {
|
||||
playerElement?.pause();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await loadVideo();
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
interfaceSearchSuggestionsStore,
|
||||
searchHistoryStore
|
||||
} from '../store';
|
||||
import { AndroidTv } from 'capacitor-android-tv-plugin';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -21,6 +22,8 @@
|
||||
|
||||
let showSearchBox = $state(false);
|
||||
|
||||
let isAndroidTv = $state(false);
|
||||
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
function debouncedSearch(event: any) {
|
||||
if (!searchSuggestions) return;
|
||||
@@ -88,7 +91,8 @@
|
||||
showSearchBox = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
isAndroidTv = await AndroidTv.isAndroidTv();
|
||||
Mousetrap.bind(['ctrl+k', 'command+k'], () => {
|
||||
document.getElementById('search-box')?.focus();
|
||||
showSearchBox = !showSearchBox;
|
||||
@@ -105,10 +109,15 @@
|
||||
await tick();
|
||||
document.getElementById('search')?.focus();
|
||||
}}
|
||||
tabindex="0"
|
||||
role="presentation"
|
||||
>
|
||||
<div class="field prefix fill no-margin search rounded">
|
||||
<i class="front">search</i>
|
||||
<div
|
||||
class="field prefix fill no-margin rounded"
|
||||
class:search={!isAndroidTv}
|
||||
class:search-tv={isAndroidTv}
|
||||
>
|
||||
<i class="front" tabindex="-1">search</i>
|
||||
<input
|
||||
id="search-box"
|
||||
placeholder={$_('searchPlaceholder')}
|
||||
@@ -175,6 +184,11 @@
|
||||
.search {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.search-tv {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--surface-variant);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
<nav class="tabbed small">
|
||||
<nav class="tabbed small" tabindex="0" role="region">
|
||||
<a class:active={activeTab === 'interface'} onclick={() => (activeTab = 'interface')}>
|
||||
<i>grid_view</i>
|
||||
<span>{$_('layout.interface')}</span>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
synciousInstanceStore,
|
||||
synciousStore
|
||||
} from '../store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AndroidTv } from 'capacitor-android-tv-plugin';
|
||||
|
||||
interface Props {
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
@@ -151,9 +153,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:sideways-root={sideways}>
|
||||
<div onfocus={() => {}} id="thumbnail-container" role="region">
|
||||
<div
|
||||
class:sideways-root={sideways}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
onclick={async () => {
|
||||
if (await AndroidTv.isAndroidTv()) {
|
||||
goto(watchUrl.toString());
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div id="thumbnail-container">
|
||||
<a
|
||||
tabindex="-1"
|
||||
class="wave thumbnail"
|
||||
href={watchUrl.toString()}
|
||||
data-sveltekit-preload-data="off"
|
||||
@@ -199,6 +211,7 @@
|
||||
<div class="thumbnail-details video-title">
|
||||
<div class="video-title">
|
||||
<a
|
||||
tabindex="-1"
|
||||
style="padding-left: 1px;"
|
||||
class="video-title"
|
||||
data-sveltekit-preload-data="off"
|
||||
@@ -208,7 +221,9 @@
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<a class:author={!sideways} href={`/channel/${video.authorId}`}>{video.author}</a>
|
||||
<a tabindex="-1" class:author={!sideways} href={`/channel/${video.authorId}`}
|
||||
>{video.author}</a
|
||||
>
|
||||
{#if !('publishedText' in video) && 'viewCountText' in video}
|
||||
<span style="margin-top: 1em;">
|
||||
• {video.viewCountText}
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
}
|
||||
|
||||
main.root {
|
||||
padding-left: 7em !important;
|
||||
padding-top: 5em !important;
|
||||
padding-top: 6em !important;
|
||||
padding-bottom: 5em !important;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
main.root-not-tv {
|
||||
padding-left: 7em !important;
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 1em !important;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"copyUrl": "Copy URL",
|
||||
"loadMore": "Load more",
|
||||
"views": "views",
|
||||
"login": "Login",
|
||||
"invidiousLogin": "Please log in with your Invidious account",
|
||||
"invalidInstance": "Please verify the URL. If it's correct, the instance may be down or may not support third-party clients.",
|
||||
"invidiousBlockWarning": "Invidious is currently being blocked by Google. If videos aren't loading for this instance, please use this instance on Materialious on {android} or {desktop} to get around this with local video fallback.",
|
||||
"videos": "videos",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type Peer from 'peerjs';
|
||||
import { get } from 'svelte/store';
|
||||
import { instanceStore, interfaceAllowInsecureRequests } from './store';
|
||||
import type { Channel, HashTag, Playlist, PlaylistPageVideo, Video, VideoBase } from './api/model';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
export function truncate(value: string, maxLength: number = 50): string {
|
||||
return value.length > maxLength ? `${value.substring(0, maxLength)}...` : value;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
instanceStore,
|
||||
interfaceAmoledTheme,
|
||||
interfaceDefaultPage,
|
||||
showWarningStore,
|
||||
syncPartyPeerStore,
|
||||
themeColorStore
|
||||
} from '$lib/store';
|
||||
@@ -35,11 +34,15 @@
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
import { AndroidTv } from 'capacitor-android-tv-plugin';
|
||||
import Mousetrap from 'mousetrap';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let mobileSearchShow = $state(false);
|
||||
|
||||
let isAndroidTv = $state(false);
|
||||
|
||||
let isLoggedIn = $state(false);
|
||||
authStore.subscribe((value) => {
|
||||
isLoggedIn = value !== null;
|
||||
@@ -94,21 +97,67 @@
|
||||
});
|
||||
|
||||
async function login() {
|
||||
const path = new URL(`${get(instanceStore)}/authorize_token`);
|
||||
const searchParams = new URLSearchParams({
|
||||
scopes: ':feed,:subscriptions*,:playlists*,:history*,:notifications*'
|
||||
});
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
searchParams.set('callback_url', 'materialious-auth://');
|
||||
path.search = searchParams.toString();
|
||||
await Browser.open({ url: path.toString() });
|
||||
if (!isAndroidTv) {
|
||||
const path = new URL(`${get(instanceStore)}/authorize_token`);
|
||||
const searchParams = new URLSearchParams({
|
||||
scopes: ':feed,:subscriptions*,:playlists*,:history*,:notifications*'
|
||||
});
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
searchParams.set('callback_url', 'materialious-auth://');
|
||||
path.search = searchParams.toString();
|
||||
await Browser.open({ url: path.toString() });
|
||||
} else {
|
||||
searchParams.set('callback_url', `${location.origin}/auth`);
|
||||
path.search = searchParams.toString();
|
||||
document.location.href = path.toString();
|
||||
}
|
||||
} else {
|
||||
searchParams.set('callback_url', `${location.origin}/auth`);
|
||||
path.search = searchParams.toString();
|
||||
document.location.href = path.toString();
|
||||
await ui('#tv-login');
|
||||
document.getElementById('username')?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
let loginError: boolean = $state(false);
|
||||
let rawUsername: string = $state('');
|
||||
let rawPassword: string = $state('');
|
||||
async function usernamePasswordLogin(event: Event) {
|
||||
event.preventDefault();
|
||||
loginError = false;
|
||||
|
||||
const body = new FormData();
|
||||
body.append('email', rawUsername);
|
||||
body.append('password', rawPassword);
|
||||
body.append('action', 'signin');
|
||||
|
||||
const response = await fetch(`${$instanceStore}/login?type=invidious`, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
headers: {
|
||||
__redirect: 'manual',
|
||||
__custom_return: 'json-headers'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const headers = await response.json();
|
||||
if ('set-cookie' in headers) {
|
||||
const sid = (headers['set-cookie'][0].split(';') as string[]).find((cookie) =>
|
||||
cookie.startsWith('SID=')
|
||||
);
|
||||
|
||||
if (sid) {
|
||||
console.log(sid);
|
||||
authStore.set({ username: rawUsername, token: sid });
|
||||
await ui('#tv-login');
|
||||
goto('/', { replaceState: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loginError = true;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authStore.set(null);
|
||||
goto('/');
|
||||
@@ -122,6 +171,8 @@
|
||||
onMount(async () => {
|
||||
ui();
|
||||
|
||||
isAndroidTv = await AndroidTv.isAndroidTv();
|
||||
|
||||
document.addEventListener('click', linkClickOverwrite);
|
||||
|
||||
loadSettingsFromEnv();
|
||||
@@ -172,20 +223,22 @@
|
||||
{@html webManifestLink}
|
||||
</svelte:head>
|
||||
|
||||
<nav class="left m l small">
|
||||
<header></header>
|
||||
{#each getPages() as navPage}
|
||||
{#if !navPage.requiresAuth || isLoggedIn}
|
||||
<a
|
||||
href={navPage.href}
|
||||
class:active={$page.url.href.endsWith(navPage.href)}
|
||||
data-sveltekit-preload-data="off"
|
||||
><i>{navPage.icon}</i>
|
||||
<div>{navPage.name}</div>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
{#if !isAndroidTv}
|
||||
<nav class="left m l small">
|
||||
<header></header>
|
||||
{#each getPages() as navPage}
|
||||
{#if !navPage.requiresAuth || isLoggedIn}
|
||||
<a
|
||||
href={navPage.href}
|
||||
class:active={$page.url.href.endsWith(navPage.href)}
|
||||
data-sveltekit-preload-data="off"
|
||||
><i>{navPage.icon}</i>
|
||||
<div>{navPage.name}</div>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<nav class="top">
|
||||
{#if !mobileSearchShow}
|
||||
@@ -299,28 +352,23 @@
|
||||
{/if}
|
||||
</dialog>
|
||||
|
||||
<main class="responsive max root">
|
||||
{#if Capacitor.getPlatform() === 'web' && $showWarningStore}
|
||||
<dialog class="active small" style="height: fit-content;">
|
||||
<h5>Warning</h5>
|
||||
<div>
|
||||
<p>
|
||||
{@html $_('invidiousBlockWarning', {
|
||||
values: {
|
||||
android:
|
||||
'<a href="https://github.com/Materialious/Materialious?tab=readme-ov-file#android" target="_blank" class="link" rel="noopener noreferrer">Android</a>',
|
||||
desktop:
|
||||
'<a href="https://github.com/Materialious/Materialious?tab=readme-ov-file#desktop-windowsmacoslinux" target="_blank" class="link" rel="noopener noreferrer">Desktop</a>'
|
||||
}
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<nav class="right-align no-space">
|
||||
<button class="transparent link" onclick={() => showWarningStore.set(false)}
|
||||
>Don't show again</button
|
||||
>
|
||||
</nav>
|
||||
</dialog>
|
||||
<main class="responsive max root" tabindex="0" role="region" class:main.root-not-tv={!isAndroidTv}>
|
||||
{#if isAndroidTv}
|
||||
<div class="tabs">
|
||||
{#each getPages() as navPage}
|
||||
{#if !navPage.requiresAuth || isLoggedIn}
|
||||
<a
|
||||
href={navPage.href}
|
||||
class:active={$page.url.href.endsWith(navPage.href)}
|
||||
class="active"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<i>{navPage.icon}</i>
|
||||
<span>{navPage.name}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $navigating}
|
||||
<PageLoading />
|
||||
@@ -331,6 +379,33 @@
|
||||
<SyncParty />
|
||||
</main>
|
||||
|
||||
<dialog class="modal" id="tv-login">
|
||||
<h5>{$_('loginRequired')}</h5>
|
||||
<div>{$_('invidiousLogin')}</div>
|
||||
|
||||
<form onsubmit={usernamePasswordLogin}>
|
||||
<div class="field label border" class:invalid={loginError}>
|
||||
<input id="username" bind:value={rawUsername} name="username" type="text" />
|
||||
<label for="username">Username</label>
|
||||
</div>
|
||||
<div class="field label border" class:invalid={loginError}>
|
||||
<input bind:value={rawPassword} name="password" type="password" />
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
|
||||
<nav class="right-align no-space">
|
||||
<button
|
||||
class="transparent link"
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
await ui('#tv-login');
|
||||
}}>{$_('cancel')}</button
|
||||
>
|
||||
<button class="transparent link" type="submit">{$_('login')}</button>
|
||||
</nav>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
nav.left a {
|
||||
font-size: 0.8em;
|
||||
|
||||
@@ -26,7 +26,10 @@ const CORS_HEADERS = [
|
||||
'X-Goog-FieldMask',
|
||||
'Range',
|
||||
'Referer',
|
||||
'Cookie'
|
||||
'Cookie',
|
||||
'__redirect',
|
||||
'__custom_return',
|
||||
'__sid_auth'
|
||||
].join(', ');
|
||||
|
||||
const CORS_ORIGIN = 'https://www.youtube.com';
|
||||
@@ -47,6 +50,11 @@ function proxyRequest(clientReq, clientRes, parsedUrl, bodyChunks = [], redirect
|
||||
return clientRes.end('Too many redirects');
|
||||
}
|
||||
|
||||
const redirectAllowed = clientReq.headers.__redirect !== 'manual';
|
||||
delete clientReq.headers.__redirect;
|
||||
const returnHeadersAsJson = clientReq.headers.__custom_return === 'json-headers';
|
||||
delete clientReq.headers.__custom_return;
|
||||
|
||||
const isHttps = parsedUrl.protocol === 'https:';
|
||||
const httpClient = isHttps ? https : http;
|
||||
|
||||
@@ -62,6 +70,11 @@ function proxyRequest(clientReq, clientRes, parsedUrl, bodyChunks = [], redirect
|
||||
rejectUnauthorized: rejectUnauthorized
|
||||
};
|
||||
|
||||
if (clientReq.headers.__sid_auth) {
|
||||
proxyOptions.headers.cookie = clientReq.headers.__sid_auth;
|
||||
delete proxyOptions.headers.__sid_auth;
|
||||
}
|
||||
|
||||
// Remove headers that may cause issues or be auto-managed
|
||||
for (const key of [
|
||||
'referer',
|
||||
@@ -75,13 +88,37 @@ function proxyRequest(clientReq, clientRes, parsedUrl, bodyChunks = [], redirect
|
||||
}
|
||||
|
||||
const proxyReq = httpClient.request(parsedUrl, proxyOptions, (proxyRes) => {
|
||||
// Handle redirects
|
||||
if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400 && proxyRes.headers.location) {
|
||||
// Handle redirects normally if redirectAllowed
|
||||
if (
|
||||
proxyRes.statusCode >= 300 &&
|
||||
proxyRes.statusCode < 400 &&
|
||||
proxyRes.headers.location &&
|
||||
redirectAllowed
|
||||
) {
|
||||
proxyRes.resume(); // discard response data
|
||||
const newUrl = new URL(proxyRes.headers.location, parsedUrl);
|
||||
return proxyRequest(clientReq, clientRes, newUrl, bodyChunks, redirectCount + 1);
|
||||
}
|
||||
|
||||
if (returnHeadersAsJson) {
|
||||
const headersDict = {};
|
||||
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
||||
headersDict[key] = value;
|
||||
}
|
||||
|
||||
const body = JSON.stringify(headersDict);
|
||||
|
||||
clientRes.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': CORS_ORIGIN,
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': CORS_HEADERS,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
});
|
||||
return clientRes.end(body);
|
||||
}
|
||||
|
||||
clientRes.writeHead(proxyRes.statusCode, {
|
||||
...proxyRes.headers,
|
||||
'Access-Control-Allow-Origin': CORS_ORIGIN,
|
||||
|
||||
Reference in New Issue
Block a user