diff --git a/materialious/electron/src/setup.ts b/materialious/electron/src/setup.ts index e9089d23..9edbbe96 100644 --- a/materialious/electron/src/setup.ts +++ b/materialious/electron/src/setup.ts @@ -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); diff --git a/materialious/package-lock.json b/materialious/package-lock.json index 68dd8163..91874a56 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -1,12 +1,12 @@ { "name": "materialious", - "version": "1.13.0", + "version": "1.13.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "materialious", - "version": "1.13.0", + "version": "1.13.2", "hasInstallScript": true, "dependencies": { "@capacitor-community/electron": "^5.0.1", @@ -24,6 +24,7 @@ "@macfja/svelte-persistent-store": "^2.4.2", "beercss": "^3.13.1", "bgutils-js": "^3.2.0", + "buffer": "^6.0.3", "capacitor-music-controls-plugin": "^6.1.0", "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", "dayjs": "^1.11.19", @@ -42,7 +43,8 @@ "shaka-player": "^4.16.11", "sponsorblock-api": "^0.2.4", "svelte-infinite-loading": "^1.4.0", - "youtubei.js": "^16.0.1" + "youtubei.js": "^16.0.1", + "zod": "^4.3.5" }, "devDependencies": { "@capacitor/assets": "^3.0.5", @@ -4951,6 +4953,30 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5045,9 +5071,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -5065,7 +5091,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-crc32": { @@ -14035,6 +14061,15 @@ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "license": "MIT" + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/materialious/package.json b/materialious/package.json index ac476f97..9bda8ea2 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -57,6 +57,7 @@ "@macfja/svelte-persistent-store": "^2.4.2", "beercss": "^3.13.1", "bgutils-js": "^3.2.0", + "buffer": "^6.0.3", "capacitor-music-controls-plugin": "^6.1.0", "capacitor-nodejs": "https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.9/capacitor-nodejs.tgz", "dayjs": "^1.11.19", @@ -75,6 +76,7 @@ "shaka-player": "^4.16.11", "sponsorblock-api": "^0.2.4", "svelte-infinite-loading": "^1.4.0", - "youtubei.js": "^16.0.1" + "youtubei.js": "^16.0.1", + "zod": "^4.3.5" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/components/ShareVideo.svelte b/materialious/src/lib/components/ShareVideo.svelte index a5c2e5cb..9ec2c67e 100644 --- a/materialious/src/lib/components/ShareVideo.svelte +++ b/materialious/src/lib/components/ShareVideo.svelte @@ -1,13 +1,12 @@ @@ -46,9 +42,9 @@ role="presentation" onclick={async () => { if (Capacitor.isNativePlatform()) { - shareUrl(`${get(instanceStore)}/watch/${video.videoId}`); + shareVideo(`${get(instanceStore)}/watch/${video.videoId}`); } else { - shareUrl( + shareVideo( `${location.origin}${resolve('/watch/[videoId]', { videoId: video.videoId })}`, 'time' ); @@ -62,7 +58,7 @@ class="row" role="presentation" onclick={async () => { - shareUrl(`https://redirect.invidious.io/watch?v=${video.videoId}`); + shareVideo(`https://redirect.invidious.io/watch?v=${video.videoId}`); }} >
{$_('player.share.invidiousRedirect')}
@@ -72,7 +68,7 @@ class="row" role="presentation" onclick={async () => { - shareUrl(`https://www.youtube.com/watch?v=${video.videoId}`); + shareVideo(`https://www.youtube.com/watch?v=${video.videoId}`); }} >
{$_('player.share.youtubeLink')}
diff --git a/materialious/src/lib/components/SyncParty.svelte b/materialious/src/lib/components/SyncParty.svelte index b7e6848f..2945215f 100644 --- a/materialious/src/lib/components/SyncParty.svelte +++ b/materialious/src/lib/components/SyncParty.svelte @@ -2,15 +2,34 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { page } from '$app/stores'; - import { peerJs, removeWindowQueryFlag, setWindowQueryFlag } from '$lib/misc'; + import { + determinePeerJsInstance, + peerJs, + removeWindowQueryFlag, + setWindowQueryFlag, + shareURL + } from '$lib/misc'; import type { PlayerEvents } from '$lib/player'; - import { Clipboard } from '@capacitor/clipboard'; import ui from 'beercss'; import type { DataConnection } from 'peerjs'; import { onDestroy, onMount } from 'svelte'; import { _ } from '$lib/i18n'; import { get } from 'svelte/store'; import { syncPartyConnectionsStore, syncPartyPeerStore } from '../store'; + import z from 'zod'; + import { Buffer } from 'buffer'; + import { Capacitor } from '@capacitor/core'; + + const SyncInfo = z.object({ + host: z.string(), + path: z.string(), + port: z.number(), + syncId: z.string() + }); + + let syncPartyManualInput: string = $state(''); + + const syncOrigin = Capacitor.isNativePlatform() ? 'https://sync.materialio.us' : location.origin; function changeVideoEvent(conn: DataConnection) { conn.on('data', (data) => { @@ -37,6 +56,16 @@ }); } + function createShareBase64(): string { + return Buffer.from( + JSON.stringify({ + ...determinePeerJsInstance(), + syncId: $syncPartyPeerStore?.id + }), + 'utf-8' + ).toString('base64'); + } + async function startWatchSync() { if ($syncPartyPeerStore) { $syncPartyPeerStore.destroy(); @@ -89,24 +118,36 @@ } }); + async function joinSyncParty(syncInfo: string) { + const parseInfo = SyncInfo.safeParse( + JSON.parse(Buffer.from(syncInfo, 'base64').toString('utf-8')) + ); + + if (parseInfo.error) { + return; + } + + $syncPartyPeerStore = await peerJs(crypto.randomUUID(), parseInfo.data); + $syncPartyPeerStore.on('open', () => { + if (!$syncPartyPeerStore) return; + + const conn = $syncPartyPeerStore.connect(parseInfo.data.syncId); + syncPartyConnectionsStore.set([...($syncPartyConnectionsStore || []), conn]); + + changeVideoEvent(conn); + }); + + $syncPartyPeerStore.on('disconnected', () => { + syncPartyPeerStore.set(null); + syncPartyConnectionsStore.set(null); + }); + } + onMount(async () => { const currentUrl = get(page).url; - const syncId = currentUrl.searchParams.get('sync'); - if (syncId) { - $syncPartyPeerStore = await peerJs(crypto.randomUUID()); - $syncPartyPeerStore.on('open', () => { - if (!$syncPartyPeerStore) return; - - const conn = $syncPartyPeerStore.connect(syncId); - syncPartyConnectionsStore.set([...($syncPartyConnectionsStore || []), conn]); - - changeVideoEvent(conn); - }); - - $syncPartyPeerStore.on('disconnected', () => { - syncPartyPeerStore.set(null); - syncPartyConnectionsStore.set(null); - }); + const syncInfo = currentUrl.searchParams.get('sync'); + if (syncInfo) { + await joinSyncParty(syncInfo); } }); @@ -118,25 +159,23 @@ }); - -
{$_('layout.syncParty')}
-

{$_('layout.syncPartyWarning')}

+ +
{$_('layout.syncParty')}
+

{$_('layout.syncPartyWarning')}

{#if $syncPartyPeerStore} {/if}
+
diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json index eba4e76a..90354b43 100644 --- a/materialious/src/lib/i18n/locales/en.json +++ b/materialious/src/lib/i18n/locales/en.json @@ -146,6 +146,8 @@ "syncPartyWarning": "Please note your IP will be visible to users you invite.", "startSyncParty": "Start sync party", "endSyncParty": "End sync party", + "joinSyncParty": "Enter sync party URL", + "shareURL": "Share URL", "notifications": "Notifications", "noNewNotifications": "No new notifications here", "settings": "Settings", diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index c2239271..49c559a2 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -3,7 +3,11 @@ import { resolve } from '$app/paths'; import he from 'he'; import type Peer from 'peerjs'; import { get } from 'svelte/store'; -import { interfaceAllowInsecureRequests, isAndroidTvStore } from './store'; +import { + interfaceAllowInsecureRequests, + interfaceAndroidUseNativeShare, + isAndroidTvStore +} from './store'; import type { Channel, HashTag, @@ -14,6 +18,9 @@ import type { VideoBase } from './api/model'; import { page } from '$app/state'; +import { Capacitor } from '@capacitor/core'; +import { Share } from '@capacitor/share'; +import { Clipboard } from '@capacitor/clipboard'; export function isVideoID(videoId: string): boolean { const regExp = /^[a-zA-Z0-9_-]{11}$/; @@ -46,16 +53,41 @@ export function removeWindowQueryFlag(key: string) { } let PeerInstance: typeof Peer; -export async function peerJs(peerId: string): Promise { +export interface PeerInstance { + host: string; + path: string; + port: number; +} + +export function determinePeerJsInstance(): PeerInstance { + return { + 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 async function peerJs( + id: string, + instance: PeerInstance = determinePeerJsInstance() +): Promise { // https://github.com/peers/peerjs/issues/819 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(id, instance); +} + +export async function shareURL(url: string) { + if ( + Capacitor.getPlatform() === 'android' && + (await Share.canShare()).value && + get(interfaceAndroidUseNativeShare) + ) { + await Share.share({ url: url }); + } else { + await Clipboard.write({ string: url }); + } } export function ensureNoTrailingSlash(url: any): string { diff --git a/materialious/src/routes/(app)/+layout.svelte b/materialious/src/routes/(app)/+layout.svelte index ad2c5d83..2e8ee47c 100644 --- a/materialious/src/routes/(app)/+layout.svelte +++ b/materialious/src/routes/(app)/+layout.svelte @@ -323,12 +323,10 @@ (mobileSearchShow = false)} />
{:else} - {#if !Capacitor.isNativePlatform()} - - {/if} + {#if isLoggedIn}