mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-11 17:45:32 +00:00
feat: w2g chat
This commit is contained in:
parent
eea3a3ea4b
commit
cb1c591b17
11 changed files with 436 additions and 162 deletions
|
|
@ -28,7 +28,7 @@
|
|||
if (anilistClient.userID?.viewer?.data?.Viewer) {
|
||||
$logout = true
|
||||
} else {
|
||||
IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=20321&response_type=token') // Change redirect_url to miru://auth/
|
||||
IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=20321&response_type=token') // Change redirect_url to migu://auth/
|
||||
if (platformMap[window.version.platform] === 'Linux') {
|
||||
toast('Support Notification', {
|
||||
description: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ function createSections () {
|
|||
{ title: 'Romance', variables: { sort: 'TRENDING_DESC', genre: 'Romance' } },
|
||||
{ title: 'Action', variables: { sort: 'TRENDING_DESC', genre: 'Action' } },
|
||||
{ title: 'Adventure', variables: { sort: 'TRENDING_DESC', genre: 'Adventure' } },
|
||||
{ title: 'Fantasy', variables: { sort: 'TRENDING_DESC', genre: 'Fantasy' } },
|
||||
{ title: 'Comedy', variables: { sort: 'TRENDING_DESC', genre: 'Comedy' } }
|
||||
{ title: 'Fantasy', variables: { sort: 'TRENDING_DESC', genre: 'Fantasy' } }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@
|
|||
}
|
||||
|
||||
function setDiscordRPC (np = nowPlaying.value) {
|
||||
const w2g = state.value
|
||||
const w2g = state.value?.code
|
||||
const details = [np.title, np.episodeTitle].filter(i => i).join(' - ') || undefined
|
||||
const activity = {
|
||||
details,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@
|
|||
|
||||
const emit = createEventDispatcher()
|
||||
|
||||
w2gEmitter.on('playerupdate', ({ detail }) => {
|
||||
w2gEmitter.on('playerupdate', detail => {
|
||||
currentTime = detail.time
|
||||
paused = detail.paused
|
||||
})
|
||||
|
||||
w2gEmitter.on('setindex', ({ detail }) => {
|
||||
w2gEmitter.on('setindex', detail => {
|
||||
playFile(detail)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0 ml-10' use:click={toggleStatus} disabled={!alToken}>
|
||||
<Bookmark fill={media.mediaListEntry ? 'currentColor' : 'transparent'} size='1.7rem' />
|
||||
</button>
|
||||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0 ml-10' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
|
||||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0 ml-10' use:click={() => copyToClipboard(`https://miguapp.pages.dev/anime/${media.id}`)}>
|
||||
<Share2 size='1.7rem' />
|
||||
</button>
|
||||
<button class='btn bg-dark btn-lg btn-square d-flex align-items-center justify-content-center shadow-none border-0 ml-10' use:click={() => openInBrowser(`https://anilist.co/anime/${media.id}`)}>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,94 @@
|
|||
<script>
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { ExternalLink, User } from 'lucide-svelte'
|
||||
export let peers
|
||||
import User from './User.svelte'
|
||||
import Message from './Message.svelte'
|
||||
import { SendHorizontal, DoorOpen, UserPlus } from 'lucide-svelte'
|
||||
export let invite
|
||||
export let cleanup
|
||||
export let state
|
||||
|
||||
function cleanup () {
|
||||
state.value?.destroy()
|
||||
state.value = null
|
||||
}
|
||||
|
||||
$: peers = $state?.peers
|
||||
$: messages = $state?.messages
|
||||
|
||||
/**
|
||||
* @param {{ message: string, user: import('@/modules/al.d.ts').Viewer | {id: string }, type: 'incoming' | 'outgoing', date: Date }[]} messages
|
||||
*/
|
||||
function groupMessages (messages) {
|
||||
if (!messages?.length) return []
|
||||
const grouped = []
|
||||
for (const { message, user, type, date } of messages.slice(-50)) {
|
||||
const last = grouped[grouped.length - 1]
|
||||
if (last && last.user.id === user.id) {
|
||||
last.messages.push(message)
|
||||
} else {
|
||||
grouped.push({ user, messages: [message], type, date })
|
||||
}
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
let message = ''
|
||||
let rows = 1
|
||||
|
||||
function sendMessage () {
|
||||
if (message) {
|
||||
state.value.message(message.trim())
|
||||
message = ''
|
||||
rows = 1
|
||||
}
|
||||
}
|
||||
|
||||
function checkInput (e) {
|
||||
if (e.key === 'Enter' && e.shiftKey === false && message) {
|
||||
sendMessage()
|
||||
} else {
|
||||
rows = message.split('\n').length || 1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='d-flex flex-column py-20 root container card'>
|
||||
<div class='d-flex align-items-center w-full'>
|
||||
<h1 class='font-weight-bold mr-auto'>Lobby</h1>
|
||||
<button class='btn btn-success btn-lg ml-20' type='button' use:click={invite}>Invite To Lobby</button>
|
||||
<button class='btn btn-danger ml-20 btn-lg' type='button' use:click={cleanup}>Leave lobby</button>
|
||||
<div class='d-flex flex-column root w-full position-relative px-md-20 h-full overflow-hidden'>
|
||||
<div class='d-flex flex-md-row flex-column-reverse w-full h-full pt-20'>
|
||||
<div class='d-flex flex-column justify-content-end overflow-hidden flex-grow-1 px-20 pb-md-20'>
|
||||
{#each groupMessages($messages) as { user, messages, type, date }}
|
||||
<Message time={date} {user} {messages} {type} />
|
||||
{/each}
|
||||
<div class='d-flex mt-20'>
|
||||
<button class='btn text-danger d-flex mt-auto align-items-center justify-content-center mr-10 border-0 px-0 shadow-none' type='button' use:click={cleanup} style='height: 3.75rem !important; width: 3.75rem !important;'>
|
||||
<DoorOpen size='1.8rem' strokeWidth={2.5} />
|
||||
</button>
|
||||
<button class='btn text-success d-flex mt-auto align-items-center justify-content-center mr-20 border-0 px-0 shadow-none' type='button' use:click={invite} style='height: 3.75rem !important; width: 3.75rem !important;'>
|
||||
<UserPlus size='1.8rem' strokeWidth={2.5} />
|
||||
</button>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
class='form-control h-auto mt-20 px-15 d-flex align-items-center justify-content-center line-height-normal w-auto flex-grow-1 shadow-0'
|
||||
{rows}
|
||||
style='resize: none; min-height: 0 !important'
|
||||
autocomplete='off'
|
||||
maxlength='2048'
|
||||
placeholder='Message' on:keyup={checkInput} />
|
||||
<button class='btn d-flex mt-auto align-items-center justify-content-center ml-20 border-0 px-0 shadow-none' type='button' use:click={sendMessage} style='height: 3.75rem !important; width: 3.75rem !important;'>
|
||||
<SendHorizontal size='1.8rem' strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='d-flex flex-column w-350 mw-full px-20'>
|
||||
{#if peers}
|
||||
<div class='font-size-20 font-weight-bold pl-5 pb-10'>
|
||||
{Object.values($peers).length} Member(s)
|
||||
</div>
|
||||
{#each Object.values($peers) as user}
|
||||
<User {user} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#each Object.values(peers) as peer}
|
||||
<!-- {#each Object.values($peers) as peer}
|
||||
<div class='d-flex align-items-center pb-10'>
|
||||
{#if peer.user?.avatar?.medium}
|
||||
<img src={peer.user?.avatar?.medium} alt='avatar' class='w-50 h-50 img-fluid rounded' />
|
||||
|
|
@ -25,11 +100,5 @@
|
|||
<span class='pointer text-primary d-flex align-items-center' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}><ExternalLink size='2.5rem' /></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/each} -->
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.anon {
|
||||
font-size: 5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
47
common/views/WatchTogether/Message.svelte
Normal file
47
common/views/WatchTogether/Message.svelte
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
/** @type {import("d:/Webdevelopment/miru/common/modules/al").Viewer | {}} */
|
||||
export let user = {}
|
||||
|
||||
/** @type {string[]} */
|
||||
export let messages = []
|
||||
|
||||
/** @type {Date} */
|
||||
export let time
|
||||
|
||||
/** @type {'incoming' | 'outgoing'} */
|
||||
export let type = 'incoming'
|
||||
const incoming = type === 'incoming'
|
||||
</script>
|
||||
|
||||
<div class='message d-flex flex-row mt-15' class:flex-row={incoming} class:flex-row-reverse={!incoming}>
|
||||
<img src={user.avatar?.medium || 'https://s4.anilist.co/file/anilistcdn/user/avatar/large/default.png'} alt='ProfilePicture' class='w-50 h-50 rounded-circle p-5 mt-auto' />
|
||||
<div class='d-flex flex-column px-10 align-items-start flex-auto' class:align-items-start={incoming} class:align-items-end={!incoming}>
|
||||
<div class='pb-5 d-flex flex-row align-items-center px-5'>
|
||||
<div class='font-weight-bold font-size-18 line-height-normal'>
|
||||
{user.name || 'Anonymous'}
|
||||
</div>
|
||||
<div class='text-muted pl-10 font-size-12 line-height-normal'>
|
||||
{time.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
{#each messages as message}
|
||||
<div class='bg-dark-light py-10 px-15 rounded-top rounded-right mb-5 select-all pre-wrap' style='max-width: calc(100% - 10rem)'
|
||||
class:bg-dark-light={incoming} class:bg-accent={!incoming}
|
||||
class:rounded-right={incoming} class:rounded-left={!incoming}>
|
||||
{message}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.message {
|
||||
--base-border-radius: 1.3rem;
|
||||
}
|
||||
.bg-accent {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
</style>
|
||||
20
common/views/WatchTogether/User.svelte
Normal file
20
common/views/WatchTogether/User.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
|
||||
/** @type {import("d:/Webdevelopment/miru/common/modules/al").Viewer | {}} */
|
||||
export let user = {}
|
||||
</script>
|
||||
|
||||
<div class='d-flex align-items-center pb-10'>
|
||||
<img src={user.avatar?.medium || 'https://s4.anilist.co/file/anilistcdn/user/avatar/large/default.png'} alt='ProfilePicture' class='w-50 h-50 rounded-circle p-5 mt-auto' />
|
||||
<div class='font-size-18 line-height-normal pl-5'>
|
||||
{user.name || 'Anonymous'}
|
||||
</div>
|
||||
{#if user.name}
|
||||
<span class='pointer text-primary d-flex align-items-center ml-auto' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + user.name)}>
|
||||
<ExternalLink size='2rem' />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -1,164 +1,54 @@
|
|||
<script context='module'>
|
||||
import { EventEmitter } from 'events'
|
||||
import { writable } from 'simple-store-svelte'
|
||||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { add, client } from '@/modules/torrent.js'
|
||||
import { generateRandomHexCode } from '@/modules/util.js'
|
||||
import { client } from '@/modules/torrent.js'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { page } from '@/App.svelte'
|
||||
import P2PT from 'p2pt'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
import 'browser-event-target-emitter'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:w2g')
|
||||
|
||||
export const w2gEmitter = new EventTarget()
|
||||
export const w2gEmitter = new EventEmitter()
|
||||
|
||||
const peers = writable({})
|
||||
/** @type {import('simple-store-svelte').Writable<W2GClient | null>} */
|
||||
export const state = writable(null)
|
||||
|
||||
export const state = writable(false)
|
||||
|
||||
let p2pt = null
|
||||
|
||||
function joinLobby (code = generateRandomHexCode(16)) {
|
||||
function joinLobby (code) {
|
||||
debug('Joining lobby with code: ' + code)
|
||||
if (p2pt) cleanup()
|
||||
p2pt = new P2PT([
|
||||
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
|
||||
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
|
||||
atob('d3NzOi8vdHJhY2tlci5maWxlcy5mbTo3MDczL2Fubm91bmNl'),
|
||||
atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov')
|
||||
], code)
|
||||
p2pt.on('peerconnect', peer => {
|
||||
debug(`Peer connected: ${peer.id}`)
|
||||
const user = anilistClient.userID?.viewer?.data?.Viewer || {}
|
||||
p2pt.send(peer,
|
||||
JSON.stringify({
|
||||
type: 'init',
|
||||
id: user.id || generateRandomHexCode(16),
|
||||
user
|
||||
})
|
||||
)
|
||||
})
|
||||
p2pt.on('peerclose', peer => {
|
||||
peers.update(object => {
|
||||
debug(`Peer disconnected: ${peer.id}`)
|
||||
delete object[peer.id]
|
||||
return object
|
||||
})
|
||||
})
|
||||
p2pt.on('msg', (peer, data) => {
|
||||
debug(`Received message from ${peer.id}: ${data.type} ${data}`)
|
||||
data = typeof data === 'string' ? JSON.parse(data) : data
|
||||
switch (data.type) {
|
||||
case 'init':
|
||||
peers.update(object => {
|
||||
object[peer.id] = {
|
||||
peer,
|
||||
user: data.user
|
||||
}
|
||||
return object
|
||||
})
|
||||
break
|
||||
case 'player': {
|
||||
if (setPlayerState(data)) w2gEmitter.emit('playerupdate', data)
|
||||
break
|
||||
}
|
||||
case 'torrent': {
|
||||
if (data.hash !== playerState.hash) {
|
||||
playerState.hash = data.hash
|
||||
add(data.magnet)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'index': {
|
||||
if (playerState.index !== data.index) {
|
||||
playerState.index = data.index
|
||||
w2gEmitter.emit('setindex', data.index)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
debug('Invalid message type: ' + data.type)
|
||||
}
|
||||
}
|
||||
})
|
||||
p2pt.start()
|
||||
state.set(code)
|
||||
console.log(p2pt)
|
||||
if (state.value) state.value.destroy()
|
||||
const client = new W2GClient(code)
|
||||
state.value = client
|
||||
client.on('index', i => w2gEmitter.emit('setindex', i))
|
||||
client.on('player', state => w2gEmitter.emit('playerupdate', { time: state.time, paused: state.paused }))
|
||||
|
||||
if (!code) invite()
|
||||
}
|
||||
|
||||
function setPlayerState (detail) {
|
||||
let emit = false
|
||||
for (const key of ['paused', 'time']) {
|
||||
emit = emit || detail[key] !== playerState[key]
|
||||
playerState[key] = detail[key]
|
||||
}
|
||||
return emit
|
||||
}
|
||||
w2gEmitter.on('player', data => state.value?.playerStateChanged(data))
|
||||
w2gEmitter.on('index', index => state.value?.mediaIndexChanged(index))
|
||||
client.on('magnet', data => state.value?.magnetLink(data))
|
||||
|
||||
w2gEmitter.on('player', ({ detail }) => {
|
||||
if (setPlayerState(detail)) emit('player', detail)
|
||||
})
|
||||
|
||||
w2gEmitter.on('index', ({ detail }) => {
|
||||
if (playerState.index !== detail.index) {
|
||||
emit('index', detail)
|
||||
playerState.index = detail.index
|
||||
}
|
||||
})
|
||||
|
||||
client.on('magnet', ({ detail }) => {
|
||||
if (detail.hash !== playerState.hash) {
|
||||
playerState.hash = detail.hash
|
||||
playerState.index = 0
|
||||
emit('torrent', detail)
|
||||
}
|
||||
})
|
||||
|
||||
function emit (type, data) {
|
||||
debug(`Emitting ${type} with data: ${JSON.stringify(data)}`)
|
||||
if (p2pt) {
|
||||
for (const { peer } of Object.values(peers.value)) {
|
||||
p2pt.send(peer, { type, ...data })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const playerState = {
|
||||
paused: null,
|
||||
time: null,
|
||||
hash: null,
|
||||
index: 0
|
||||
}
|
||||
|
||||
IPC.on('w2glink', link => {
|
||||
IPC.on('w2glink', (link) => {
|
||||
joinLobby(link)
|
||||
page.set('watchtogether')
|
||||
})
|
||||
|
||||
function cleanup () {
|
||||
state.set(false)
|
||||
peers.set({})
|
||||
p2pt.destroy()
|
||||
p2pt = null
|
||||
}
|
||||
|
||||
function invite () {
|
||||
if (p2pt) {
|
||||
navigator.clipboard.writeText(`https://miguapp.pages.dev/w2g/${p2pt.identifierString}`)
|
||||
toast('Copied to clipboard', {
|
||||
description: 'Copied invite URL to clipboard',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
navigator.clipboard.writeText(state.value.inviteLink)
|
||||
toast.success('Copied to clipboard', {
|
||||
description: 'Copied invite URL to clipboard',
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Lobby from './Lobby.svelte'
|
||||
import { Plus, UserPlus } from 'lucide-svelte'
|
||||
import { W2GClient } from './w2g.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
|
||||
let joinText
|
||||
|
||||
|
|
@ -175,10 +65,10 @@
|
|||
$: checkInvite(joinText)
|
||||
</script>
|
||||
|
||||
<div class='d-flex h-full align-items-center flex-column content' style="padding-top: var(--safe-area-top)">
|
||||
<div class='font-size-50 font-weight-bold pt-20 mt-20 root'>Watch Together</div>
|
||||
<div class='d-flex h-full align-items-center flex-column px-md-20' style="padding-top: var(--safe-area-top)">
|
||||
{#if !$state}
|
||||
<div class='d-flex flex-row flex-wrap justify-content-center align-items-center h-full mb-20 pb-20 root'>
|
||||
<div class='font-size-50 font-weight-bold pt-20 mt-20 root'>Watch Together</div>
|
||||
<div class='d-flex flex-row flex-wrap justify-content-center align-items-center h-full mb-20 pb-20 root position-relative w-full'>
|
||||
<div class='card d-flex flex-column align-items-center w-300 h-300 justify-content-end'>
|
||||
<Plus size='6rem' class='d-flex align-items-center h-full' />
|
||||
<button class='btn btn-primary btn-lg mt-10 btn-block' type='button' use:click={() => joinLobby()}>Create Lobby</button>
|
||||
|
|
@ -196,7 +86,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Lobby peers={$peers} {cleanup} {invite} />
|
||||
<Lobby {state} {invite} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
16
common/views/WatchTogether/events.js
Normal file
16
common/views/WatchTogether/events.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export default class Event {
|
||||
payload = null
|
||||
type = ''
|
||||
constructor (type, payload) {
|
||||
this.type = type
|
||||
this.payload = payload
|
||||
}
|
||||
}
|
||||
|
||||
export const EventTypes = {
|
||||
SessionInitEvent: 'init',
|
||||
MagnetLinkEvent: 'magnet',
|
||||
MediaIndexEvent: 'index',
|
||||
PlayerStateEvent: 'player',
|
||||
MessageEvent: 'message'
|
||||
}
|
||||
233
common/views/WatchTogether/w2g.js
Normal file
233
common/views/WatchTogether/w2g.js
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import { EventEmitter } from 'events'
|
||||
|
||||
import P2PT from 'p2pt'
|
||||
|
||||
import Event, { EventTypes } from './events.js'
|
||||
import { anilistClient } from '@/modules/anilist.js'
|
||||
import { add } from '@/modules/torrent.js'
|
||||
import { generateRandomHexCode } from '@/modules/util.js'
|
||||
import { writable } from 'simple-store-svelte'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('ui:w2g')
|
||||
|
||||
/**
|
||||
* @typedef {Record<string, {user: import('@/modules/al.d.ts').Viewer | {id: string }, peer?: import('p2pt').Peer<any>}>} PeerList
|
||||
*/
|
||||
|
||||
export class W2GClient extends EventEmitter {
|
||||
static #announce = [
|
||||
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
|
||||
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
|
||||
atob('d3NzOi8vdHJhY2tlci5maWxlcy5mbTo3MDczL2Fubm91bmNl'),
|
||||
atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov')
|
||||
]
|
||||
|
||||
player = {
|
||||
paused: true,
|
||||
time: 0
|
||||
}
|
||||
|
||||
index = 0
|
||||
/** @type {{maget: 'string', hash: 'string'} | null} */
|
||||
magnet = null
|
||||
isHost = false
|
||||
#p2pt
|
||||
code
|
||||
/** @type {import('simple-store-svelte').Writable<{message: string, user: import('@/modules/al.d.ts').Viewer | {id: string }, type: 'incoming' | 'outgoing', date: Date}[]>} */
|
||||
messages = writable([])
|
||||
|
||||
self = anilistClient.userID?.viewer.data.Viewer || { id: generateRandomHexCode(16) }
|
||||
/** @type {import('simple-store-svelte').Writable<PeerList>} */
|
||||
peers = writable({ [this.self.id]: { user: this.self } })
|
||||
|
||||
get inviteLink () {
|
||||
return `https://miguapp.pages.dev/w2g/${this.code}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when media index changed locally
|
||||
* @param {number} index
|
||||
*/
|
||||
localMediaIndexChanged (index) {
|
||||
this.index = index
|
||||
|
||||
this.mediaIndexChanged(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when player state changed locally
|
||||
* @param {import('./events.js').default} state
|
||||
*/
|
||||
localPlayerStateChanged ({ payload }) {
|
||||
debug(`localPlayerStateChanged: ${JSON.stringify(payload)}`)
|
||||
this.player.payload.paused = payload.paused
|
||||
this.player.payload.time = payload.time
|
||||
|
||||
this.playerStateChanged(this.player)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code lobby code
|
||||
*/
|
||||
constructor (code) {
|
||||
super()
|
||||
this.isHost = !code
|
||||
|
||||
this.code = code ?? generateRandomHexCode(16)
|
||||
|
||||
debug(`W2GClient: ${this.code}, ${this.isHost}`)
|
||||
|
||||
this.#p2pt = new P2PT(W2GClient.#announce, this.code)
|
||||
|
||||
this.#wireEvents()
|
||||
this.#p2pt.start()
|
||||
}
|
||||
|
||||
magnetLink (magnet) {
|
||||
debug(`magnetLink: ${this.magnet?.hash} ${magnet.hash}`)
|
||||
if (this.magnet?.hash !== magnet.hash) {
|
||||
this.magnet = magnet
|
||||
this.isHost = true
|
||||
this.#sendToPeers(new Event('magnet', magnet))
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {number} index */
|
||||
mediaIndexChanged (index) {
|
||||
debug(`mediaIndexChanged: ${this.index} ${index}`)
|
||||
if (this.index !== index) {
|
||||
this.index = index
|
||||
this.#sendToPeers(new Event('index', index))
|
||||
}
|
||||
}
|
||||
|
||||
_playerStateChanged (state) {
|
||||
debug(`_playerStateChanged: ${this.player.paused} ${state.paused} ${this.player.time} ${state.time}`)
|
||||
if (this.player.paused !== state.paused || this.player.time !== state.time) {
|
||||
this.player = state
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
playerStateChanged (state) {
|
||||
debug(`playerStateChanged: ${JSON.stringify(state)}`)
|
||||
if (this._playerStateChanged(state)) this.#sendToPeers(new Event('player', state))
|
||||
}
|
||||
|
||||
message (message) {
|
||||
debug(`message: ${message}`)
|
||||
this.messages.update(messages => [...messages, ({
|
||||
message,
|
||||
user: this.self,
|
||||
type: 'outgoing',
|
||||
date: new Date()
|
||||
})])
|
||||
this.#sendToPeers(new Event('message', message))
|
||||
}
|
||||
|
||||
#wireEvents () {
|
||||
this.#p2pt.on('peerconnect', this.#onPeerconnect.bind(this))
|
||||
this.#p2pt.on('msg', this.#onMsg.bind(this))
|
||||
this.#p2pt.on('peerclose', this.#onPeerclose.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('p2pt').Peer} peer
|
||||
* @param {import('./events.js').default} event
|
||||
*/
|
||||
#sendEvent (peer, event) {
|
||||
debug(`#sendEvent: ${peer.id} ${JSON.stringify(event)}`)
|
||||
this.#p2pt?.send(peer, JSON.stringify(event))
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called only on 'peerconnect'
|
||||
* @param {import('p2pt').Peer} peer
|
||||
*/
|
||||
#sendInitialSessionState (peer) {
|
||||
this.#sendEvent(peer, new Event('magnet', this.magnet))
|
||||
this.#sendEvent(peer, new Event('index', this.index))
|
||||
this.#sendEvent(peer, new Event('player', this.player))
|
||||
}
|
||||
|
||||
async #onPeerconnect (peer) {
|
||||
debug(`#onPeerconnect: ${peer.id}`)
|
||||
this.#sendEvent(peer, new Event('init', this.self))
|
||||
|
||||
if (this.isHost) this.#sendInitialSessionState(peer)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('p2pt').Peer} peer
|
||||
* @param {Event} data
|
||||
*/
|
||||
#onMsg (peer, data) {
|
||||
debug(`#onMsg: ${peer.id} ${JSON.stringify(data)}`)
|
||||
data = typeof data === 'string' ? JSON.parse(data) : data
|
||||
|
||||
switch (data.type) {
|
||||
case EventTypes.SessionInitEvent:
|
||||
this.peers.update(peers => {
|
||||
peers[peer.id] = {
|
||||
peer,
|
||||
user: data.payload
|
||||
}
|
||||
return peers
|
||||
})
|
||||
break
|
||||
case EventTypes.MagnetLinkEvent: {
|
||||
const { hash, magnet } = data.payload
|
||||
if (hash !== this.magnet?.hash) {
|
||||
this.isHost = false
|
||||
this.magnet = data.payload
|
||||
add(magnet)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case EventTypes.MediaIndexEvent: {
|
||||
if (this.index !== data.payload.index) {
|
||||
this.index = data.payload.index
|
||||
this.emit('index', data.payload.index)
|
||||
}
|
||||
break
|
||||
}
|
||||
case EventTypes.PlayerStateEvent: {
|
||||
if (this._playerStateChanged(data.payload)) this.emit('player', data.payload)
|
||||
break
|
||||
}
|
||||
case EventTypes.MessageEvent:{
|
||||
this.messages.update(messages => [...messages, ({ message: data.payload, user: this.peers.value[peer.id].user, type: 'incoming', date: new Date() })])
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.error('Invalid message type', data)
|
||||
}
|
||||
}
|
||||
|
||||
#onPeerclose (peer) {
|
||||
debug(`#onPeerclose: ${peer.id}`)
|
||||
this.peers.update(peers => {
|
||||
delete peers[peer.id]
|
||||
return peers
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {import('./events.js').default} event */
|
||||
#sendToPeers (event) {
|
||||
if (!this.#p2pt) return
|
||||
|
||||
for (const { peer } of Object.values(this.peers)) {
|
||||
if (peer) this.#sendEvent(peer, event)
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
debug('destroy')
|
||||
this.#p2pt.destroy()
|
||||
this.#p2pt = null
|
||||
this.isHost = false
|
||||
this.peers.value = {}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue