Improved sync party to support Android & Desktop
This commit is contained in:
@@ -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);
|
||||
|
||||
Generated
+42
-7
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { instanceStore, interfaceAndroidUseNativeShare } from '$lib/store';
|
||||
import { Share } from '@capacitor/share';
|
||||
import { instanceStore } from '$lib/store';
|
||||
import ui from 'beercss';
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Notification, PlaylistPageVideo, Video, VideoBase } from '../api/model';
|
||||
import { shareURL } from '$lib/misc';
|
||||
|
||||
interface Props {
|
||||
video: VideoBase | Video | Notification | PlaylistPageVideo;
|
||||
@@ -17,15 +16,12 @@
|
||||
let { video, currentTime = $bindable() }: Props = $props();
|
||||
let includeTimestamp: boolean = $state(false);
|
||||
|
||||
async function shareUrl(url: string, param: string = 't') {
|
||||
async function shareVideo(url: string, param: string = 't') {
|
||||
if (includeTimestamp) url += `?${param}=${Math.floor(currentTime ?? 0)}`;
|
||||
|
||||
if ((await Share.canShare()).value && $interfaceAndroidUseNativeShare) {
|
||||
await Share.share({ url: url, dialogTitle: video.title });
|
||||
} else {
|
||||
await Clipboard.write({ string: url });
|
||||
ui('#share-success');
|
||||
}
|
||||
await shareURL(url);
|
||||
|
||||
ui('#share-success');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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}`);
|
||||
}}
|
||||
>
|
||||
<div class="min">{$_('player.share.invidiousRedirect')}</div>
|
||||
@@ -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}`);
|
||||
}}
|
||||
>
|
||||
<div class="min">{$_('player.share.youtubeLink')}</div>
|
||||
|
||||
@@ -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 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<dialog id="sync-party">
|
||||
<h6>{$_('layout.syncParty')}</h6>
|
||||
<p>{$_('layout.syncPartyWarning')}</p>
|
||||
<dialog id="sync-party" class="padding">
|
||||
<h6 class="no-margin">{$_('layout.syncParty')}</h6>
|
||||
<p class="no-margin">{$_('layout.syncPartyWarning')}</p>
|
||||
{#if $syncPartyPeerStore}
|
||||
<nav>
|
||||
<div class="field label border max">
|
||||
<input
|
||||
name="sync-share"
|
||||
readonly
|
||||
value={`${location.origin}${resolve(`/?sync=${$syncPartyPeerStore?.id}`, {})}`}
|
||||
value={`${syncOrigin}${resolve(`/?sync=${createShareBase64()}`, {})}`}
|
||||
type="text"
|
||||
/>
|
||||
<label class="active" for="sync-share">Share URL</label>
|
||||
<label class="active" for="sync-share">{$_('layout.shareURL')}</label>
|
||||
</div>
|
||||
<button
|
||||
onclick={async () => {
|
||||
await Clipboard.write({
|
||||
string: `${location.origin}${resolve(`/?sync=${$syncPartyPeerStore?.id}`, {})}`
|
||||
});
|
||||
await shareURL(`${syncOrigin}${resolve(`/?sync=${createShareBase64()}`, {})}`);
|
||||
}}
|
||||
class="square round"
|
||||
>
|
||||
@@ -145,6 +184,7 @@
|
||||
</nav>
|
||||
{/if}
|
||||
<div class="space"></div>
|
||||
|
||||
<button
|
||||
class="no-margin"
|
||||
onclick={startWatchSync}
|
||||
@@ -155,6 +195,29 @@
|
||||
{$_('layout.startSyncParty')}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if !$syncPartyPeerStore}
|
||||
<div class="space"></div>
|
||||
<hr />
|
||||
<div class="space"></div>
|
||||
<h6 class="no-margin">{$_('layout.joinSyncParty')}</h6>
|
||||
<form
|
||||
onsubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await joinSyncParty(new URL(syncPartyManualInput).searchParams.get('sync') || '');
|
||||
} catch {
|
||||
// Continue regardless of error
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="field label border max">
|
||||
<input bind:value={syncPartyManualInput} type="text" name="sync-input" />
|
||||
<label for="sync-input">{$_('layout.shareURL')}</label>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</dialog>
|
||||
|
||||
<div class="snackbar" id="sync-party-connection-join">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Peer> {
|
||||
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<Peer> {
|
||||
// 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 {
|
||||
|
||||
@@ -323,12 +323,10 @@
|
||||
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
|
||||
</div>
|
||||
{:else}
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<button data-ui="#sync-party" class="circle large transparent">
|
||||
<i class:primary-text={$syncPartyPeerStore}>group</i>
|
||||
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
<button data-ui="#sync-party" class="circle large transparent">
|
||||
<i class:primary-text={$syncPartyPeerStore}>group</i>
|
||||
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
|
||||
</button>
|
||||
{#if isLoggedIn}
|
||||
<button class="circle large transparent" onclick={() => ui('#dialog-notifications')}
|
||||
><i>notifications</i>
|
||||
|
||||
Reference in New Issue
Block a user