From be8e3711ce189f522f3a61d5483aa79fb59e612a Mon Sep 17 00:00:00 2001 From: ward Date: Thu, 19 Jun 2025 14:49:59 +1200 Subject: [PATCH] Started android tv support --- materialious/android/app/build.gradle | 5 +- .../android/app/capacitor.build.gradle | 1 + .../android/app/src/main/AndroidManifest.xml | 2 + .../android/capacitor.settings.gradle | 3 + materialious/capacitor.config.ts | 24 +-- materialious/electron/package-lock.json | 4 +- materialious/package-lock.json | 14 +- materialious/package.json | 3 +- .../src/lib/android/http/androidRequests.ts | 5 +- materialious/src/lib/api/index.ts | 9 +- .../src/lib/components/ContentColumn.svelte | 15 +- materialious/src/lib/components/Player.svelte | 38 +++- materialious/src/lib/components/Search.svelte | 20 +- .../lib/components/Settings/Settings.svelte | 2 +- .../src/lib/components/Thumbnail.svelte | 21 ++- materialious/src/lib/css/global.css | 7 +- materialious/src/lib/i18n/locales/en.json | 2 + materialious/src/lib/misc.ts | 1 + materialious/src/routes/(app)/+layout.svelte | 171 +++++++++++++----- materialious/static/nodejs-android/index.js | 43 ++++- 20 files changed, 295 insertions(+), 95 deletions(-) diff --git a/materialious/android/app/build.gradle b/materialious/android/app/build.gradle index 29247dc8..e1080ac3 100644 --- a/materialious/android/app/build.gradle +++ b/materialious/android/app/build.gradle @@ -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 } } diff --git a/materialious/android/app/capacitor.build.gradle b/materialious/android/app/capacitor.build.gradle index 6bea4029..e6b4ccdf 100644 --- a/materialious/android/app/capacitor.build.gradle +++ b/materialious/android/app/capacitor.build.gradle @@ -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') } diff --git a/materialious/android/app/src/main/AndroidManifest.xml b/materialious/android/app/src/main/AndroidManifest.xml index da6049ea..6d1a5207 100644 --- a/materialious/android/app/src/main/AndroidManifest.xml +++ b/materialious/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ android:supportsRtl="true" android:usesCleartextTraffic="true" android:theme="@style/AppTheme"> + + diff --git a/materialious/android/capacitor.settings.gradle b/materialious/android/capacitor.settings.gradle index ef0cb561..841f33eb 100644 --- a/materialious/android/capacitor.settings.gradle +++ b/materialious/android/capacitor.settings.gradle @@ -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') diff --git a/materialious/capacitor.config.ts b/materialious/capacitor.config.ts index 0847dd5c..cfabd961 100644 --- a/materialious/capacitor.config.ts +++ b/materialious/capacitor.config.ts @@ -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; diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index 7a2ca20f..ffd10f47 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -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", diff --git a/materialious/package-lock.json b/materialious/package-lock.json index c630bb9d..e41926fc 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -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", diff --git a/materialious/package.json b/materialious/package.json index ad4c5e2d..a326c9ab 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/android/http/androidRequests.ts b/materialious/src/lib/android/http/androidRequests.ts index a3fe52ed..48c474d2 100644 --- a/materialious/src/lib/android/http/androidRequests.ts +++ b/materialious/src/lib/android/http/androidRequests.ts @@ -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 }); + }); } diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index f1b9911c..39440178 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -59,8 +59,13 @@ export async function fetchErrorHandle(response: Response): Promise { return response; } -export function buildAuthHeaders(): { headers: { Authorization: string } } { - return { headers: { Authorization: `Bearer ${get(authStore)?.token}` } }; +export function buildAuthHeaders(): { headers: Record } { + 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 { diff --git a/materialious/src/lib/components/ContentColumn.svelte b/materialious/src/lib/components/ContentColumn.svelte index 9b5de488..ec918675 100644 --- a/materialious/src/lib/components/ContentColumn.svelte +++ b/materialious/src/lib/components/ContentColumn.svelte @@ -1,25 +1,30 @@ -
+
{@render children?.()}
diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 4c5b3561..27ffd578 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -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) { diff --git a/materialious/src/lib/components/Search.svelte b/materialious/src/lib/components/Search.svelte index f65690db..cc800382 100644 --- a/materialious/src/lib/components/Search.svelte +++ b/materialious/src/lib/components/Search.svelte @@ -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" > -