mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-20 07:22:03 +00:00
parent
aba4083278
commit
369c698a76
12 changed files with 380 additions and 41 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Miru",
|
||||
"version": "1.6.2",
|
||||
"version": "1.7.0",
|
||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||
"main": "src/index.js",
|
||||
"homepage": "https://github.com/ThaUnknown/miru#readme",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"electron": "^16.0.10",
|
||||
"electron-builder": "^22.14.13",
|
||||
"electron-notarize": "^1.1.1",
|
||||
"svelte": "^3.46.4",
|
||||
"svelte": "^3.47.0",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-commonjs-externals": "^0.1.1"
|
||||
},
|
||||
|
|
@ -35,7 +35,9 @@
|
|||
"build": {
|
||||
"protocols": {
|
||||
"name": "miru",
|
||||
"schemes": ["miru"]
|
||||
"schemes": [
|
||||
"miru"
|
||||
]
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -75,6 +75,21 @@
|
|||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
:global(.root) {
|
||||
animation: 0.3s ease 0s 1 root-load-in;
|
||||
}
|
||||
@keyframes root-load-in {
|
||||
from {
|
||||
bottom: -1.2rem;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-wrapper.with-sidebar[data-sidebar-type~='overlayed-sm-and-down'] > :global(.content-wrapper) {
|
||||
left: var(--sidebar-minimised);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import Home from './pages/home/Home.svelte'
|
||||
import Player from './pages/Player.svelte'
|
||||
import Settings from './pages/Settings.svelte'
|
||||
import WatchTogether from './pages/watchtogether/WatchTogether.svelte'
|
||||
export let page = 'home'
|
||||
const current = getContext('gallery')
|
||||
</script>
|
||||
|
|
@ -19,6 +20,8 @@
|
|||
<Settings />
|
||||
{:else if page === 'home'}
|
||||
<Home bind:current={$current} />
|
||||
{:else if page === 'watchtogether'}
|
||||
<WatchTogether />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@
|
|||
icon: 'queue_music',
|
||||
text: 'Now Playing'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'watchtogether'
|
||||
},
|
||||
icon: 'groups',
|
||||
text: 'Watch Together'
|
||||
},
|
||||
{
|
||||
click: () => {
|
||||
page = 'settings'
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@
|
|||
}
|
||||
function getMediaMaxEp(media, playable) {
|
||||
if (playable) {
|
||||
return media.nextAiringEpisode?.episode - 1 || media.airingSchedule?.nodes?.[0].episode - 1 || media.episodes
|
||||
return media.nextAiringEpisode?.episode - 1 || media.airingSchedule?.nodes?.[0]?.episode - 1 || media.episodes
|
||||
} else {
|
||||
return media.episodes || media.nextAiringEpisode?.episode - 1 || media.airingSchedule?.nodes?.[0].episode - 1
|
||||
return media.episodes || media.nextAiringEpisode?.episode - 1 || media.airingSchedule?.nodes?.[0]?.episode - 1
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -59,13 +59,23 @@
|
|||
|
||||
<script>
|
||||
import { alEntry } from '@/modules/anilist.js'
|
||||
import { client } from '@/modules/torrent.js'
|
||||
import { resolveFileMedia } from '@/modules/anime.js'
|
||||
import Peer from '@/modules/Peer.js'
|
||||
import Subtitles from '@/modules/subtitles.js'
|
||||
import { toTS, videoRx, fastPrettyBytes } from '@/modules/util.js'
|
||||
import Keyboard from './Keyboard.svelte'
|
||||
|
||||
import { w2gEmitter } from './watchtogether/WatchTogether.svelte'
|
||||
|
||||
w2gEmitter.on('playerupdate',({detail})=>{
|
||||
currentTime = detail.time
|
||||
paused = detail.paused
|
||||
})
|
||||
|
||||
function updatew2g() {
|
||||
w2gEmitter.emit('player', { time: Math.floor(currentTime), paused })
|
||||
}
|
||||
|
||||
async function mediaChange(current, image) {
|
||||
if (current && 'mediaSession' in navigator) {
|
||||
if (!media || (!hadImage && image)) {
|
||||
|
|
@ -788,6 +798,9 @@
|
|||
bind:ended
|
||||
bind:muted
|
||||
bind:playbackRate
|
||||
on:pause={updatew2g}
|
||||
on:play={updatew2g}
|
||||
on:seeked={updatew2g}
|
||||
on:timeupdate={() => createThumbnail()}
|
||||
on:timeupdate={checkCompletion}
|
||||
on:waiting={showBuffering}
|
||||
|
|
|
|||
|
|
@ -237,21 +237,4 @@
|
|||
</div>
|
||||
</Tab>
|
||||
</div>
|
||||
</div></Tabs>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
animation: 0.3s ease 0s 1 load-in;
|
||||
}
|
||||
@keyframes load-in {
|
||||
from {
|
||||
bottom: -1.2rem;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</div></Tabs>
|
||||
|
|
@ -226,20 +226,3 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
animation: 0.3s ease 0s 1 load-in;
|
||||
}
|
||||
@keyframes load-in {
|
||||
from {
|
||||
bottom: -1.2rem;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
93
src/renderer/src/lib/pages/watchtogether/Connect.svelte
Normal file
93
src/renderer/src/lib/pages/watchtogether/Connect.svelte
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<script>
|
||||
export let state
|
||||
export let peer
|
||||
export let cancel
|
||||
|
||||
let values = []
|
||||
let code = ''
|
||||
peer.signalingPort.onmessage = ({ data }) => {
|
||||
const { description, candidate } = typeof data === 'string' ? JSON.parse(data) : data
|
||||
if (description) {
|
||||
if (description.type === 'answer') values = []
|
||||
values.push(description.sdp)
|
||||
} else if (candidate) {
|
||||
values.push(candidate.candidate)
|
||||
}
|
||||
code = btoa(JSON.stringify(values))
|
||||
}
|
||||
|
||||
$: value = (step?.mode === 'copy' && code) || null
|
||||
|
||||
function handleInput({ target }) {
|
||||
const val = JSON.parse(atob(target.value))
|
||||
const [description, ...candidates] = val
|
||||
peer.signalingPort.postMessage({
|
||||
description: {
|
||||
type: state === 'host' ? 'answer' : 'offer',
|
||||
sdp: description
|
||||
}
|
||||
})
|
||||
for (const candidate of candidates) {
|
||||
peer.signalingPort.postMessage({
|
||||
candidate: {
|
||||
candidate,
|
||||
sdpMid: '0',
|
||||
sdpMLineIndex: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
value = null
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
function copyData() {
|
||||
navigator.clipboard.writeText(value)
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
let index = 0
|
||||
$: step = map[state]?.[index]
|
||||
|
||||
let map = {
|
||||
guest: [
|
||||
{
|
||||
title: 'Paste Invite Code',
|
||||
description: 'Paste the one time invite code sent to you by the lobby host.',
|
||||
mode: 'paste'
|
||||
},
|
||||
{
|
||||
title: 'Copy Invite Confirmation',
|
||||
description: 'Send the host this code, which required to request a connection.',
|
||||
mode: 'copy'
|
||||
}
|
||||
],
|
||||
host: [
|
||||
{
|
||||
title: 'Copy Invite Code',
|
||||
description: 'Copy the one time invite code, and send it to the person you wish to invite.',
|
||||
mode: 'copy'
|
||||
},
|
||||
{
|
||||
title: 'Paste Invite Confirmation',
|
||||
description: 'Paste the code sent to you by the user which wants to join your lobby.',
|
||||
mode: 'paste'
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="h-full d-flex flex-column justify-content-center mb-20 pb-20 root container">
|
||||
{#if step}
|
||||
<h1 class="font-weight-bold">
|
||||
{step.title}
|
||||
</h1>
|
||||
<p class="font-size-18 mt-0">
|
||||
{step.description}
|
||||
</p>
|
||||
<textarea disabled={step.mode === 'copy'} on:input={handleInput} bind:value class="form-control h-300 w-full mb-15" />
|
||||
{#if step.mode === 'copy' && value}
|
||||
<button class="btn btn-primary mt-5" type="button" on:click={copyData} disabled={!value}>Copy</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<button class="btn btn-danger mt-5" type="button" on:click={cancel}>Cancel</button>
|
||||
</div>
|
||||
30
src/renderer/src/lib/pages/watchtogether/Lobby.svelte
Normal file
30
src/renderer/src/lib/pages/watchtogether/Lobby.svelte
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
export let peers
|
||||
export let state
|
||||
export let invite
|
||||
export let cleanup
|
||||
</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>
|
||||
{#if state === 'host'}
|
||||
<button class="btn btn-success btn-lg ml-20" type="button" on:click={invite}>Invite To Lobby</button>
|
||||
{/if}
|
||||
<button class="btn btn-danger ml-20 btn-lg" type="button" on:click={cleanup}>Leave lobby</button>
|
||||
</div>
|
||||
{#each Object.values(peers) as peer}
|
||||
<div class="d-flex align-items-center ">
|
||||
{#if peer.user?.avatar?.medium}
|
||||
<img src={peer.user?.avatar?.medium} alt="avatar" class="w-50 img-fluid rounded" />
|
||||
{/if}
|
||||
<h4 class="my-0 pl-20 mr-auto">{peer.user?.name || 'Anonymous'}</h4>
|
||||
{#if peer.user?.name}
|
||||
<span class="material-icons pointer text-primary" on:click={() => window.IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}> open_in_new </span>
|
||||
{/if}
|
||||
{#if state === 'host'}
|
||||
<span class="material-icons ml-15 pointer text-danger" on:click={() => peer.peer.pc.close()}> logout </span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
200
src/renderer/src/lib/pages/watchtogether/WatchTogether.svelte
Normal file
200
src/renderer/src/lib/pages/watchtogether/WatchTogether.svelte
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<script context="module">
|
||||
import { writable, get } from 'svelte/store'
|
||||
import { alID } from '@/modules/anilist.js'
|
||||
import { add } from '@/modules/torrent.js'
|
||||
import Peer from '@/modules/Peer.js'
|
||||
import { generateRandomHexCode } from '@/modules/util.js'
|
||||
import { addToast } from '@/lib/Toasts.svelte'
|
||||
|
||||
export const w2gEmitter = new EventTarget()
|
||||
w2gEmitter.emit = (type, detail) => w2gEmitter.dispatchEvent(new CustomEvent(type, { detail }))
|
||||
w2gEmitter.on = w2gEmitter.addEventListener.bind(w2gEmitter)
|
||||
|
||||
const state = writable(null)
|
||||
|
||||
const peers = writable({})
|
||||
|
||||
let peersExternal = {}
|
||||
|
||||
peers.subscribe(value => (peersExternal = value))
|
||||
|
||||
const pending = writable(null)
|
||||
|
||||
function invite() {
|
||||
pending.set(new Peer({ polite: false }))
|
||||
}
|
||||
|
||||
pending.subscribe(peer => {
|
||||
if (peer) peer.ready.then(() => handlePeer(peer))
|
||||
})
|
||||
|
||||
w2gEmitter.on('pause', ({ detail }) => {
|
||||
if (playerState.paused !== true) {
|
||||
playerState.paused = true
|
||||
playerState.time = detail.time
|
||||
emit('pause', detail)
|
||||
}
|
||||
})
|
||||
w2gEmitter.on('play', ({ detail }) => {
|
||||
if (playerState.paused !== false) {
|
||||
playerState.paused = false
|
||||
playerState.time = detail.time
|
||||
emit('play', detail)
|
||||
}
|
||||
})
|
||||
w2gEmitter.on('seeked', ({ detail }) => {
|
||||
if (playerState.time !== detail.time) {
|
||||
playerState.time = detail.time
|
||||
emit('seeked', detail)
|
||||
}
|
||||
})
|
||||
|
||||
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', ({ detail }) => {
|
||||
if (setPlayerState(detail)) emit('player', detail)
|
||||
})
|
||||
|
||||
window.IPC.on('torrent', file => {
|
||||
playerState.file = file
|
||||
emit('torrent', { file })
|
||||
})
|
||||
|
||||
function emit(type, data) {
|
||||
for (const peer of Object.values(peersExternal)) {
|
||||
peer.channel.send(JSON.stringify({ type, ...data }))
|
||||
}
|
||||
}
|
||||
|
||||
const playerState = {
|
||||
paused: null,
|
||||
time: null,
|
||||
file: []
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
pending.update(peer => {
|
||||
peer.pc.close()
|
||||
})
|
||||
state.set(null)
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (get(state)) {
|
||||
addToast({
|
||||
text: 'The lobby you were previously in has disbanded.',
|
||||
title: 'Lobby Disbanded',
|
||||
type: 'danger'
|
||||
})
|
||||
state.set(null)
|
||||
pending.set(null)
|
||||
peers.update(peers => {
|
||||
for (const peer of Object.values(peers)) peer?.peer?.pc.close()
|
||||
return {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeer(peer) {
|
||||
pending.set(null)
|
||||
if (get(state) === 'guest') peer.dc.onclose = cleanup
|
||||
// add event listeners and store in peers
|
||||
const protocolChannel = peer.pc.createDataChannel('w2gprotocol', { negotiated: true, id: 2 })
|
||||
protocolChannel.onopen = async () => {
|
||||
protocolChannel.onmessage = ({ data }) => handleMessage(JSON.parse(data), protocolChannel, peer)
|
||||
const user = (await alID)?.data?.Viewer || {}
|
||||
protocolChannel.send(
|
||||
JSON.stringify({
|
||||
type: 'init',
|
||||
id: user.id || generateRandomHexCode(16),
|
||||
user
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(data, channel, peer) {
|
||||
console.log(data)
|
||||
switch (data.type) {
|
||||
case 'init':
|
||||
{
|
||||
peers.update(object => {
|
||||
object[data.id] = {
|
||||
peer,
|
||||
channel,
|
||||
user: data.user
|
||||
}
|
||||
return object
|
||||
})
|
||||
|
||||
channel.onclose = () => {
|
||||
peers.update(object => {
|
||||
delete object[data.id]
|
||||
return object
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'player': {
|
||||
if (setPlayerState(data)) w2gEmitter.emit('playerupdate', data)
|
||||
break
|
||||
}
|
||||
case 'torrent': {
|
||||
if (!playerState.file.every((v, i) => v === data.file[i])) add(data.file)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
console.error('Invalid message type', data, channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setState(newstate) {
|
||||
if (newstate === 'guest') {
|
||||
const peer = new Peer({ polite: true })
|
||||
pending.set(peer)
|
||||
}
|
||||
state.set(newstate)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Lobby from './Lobby.svelte'
|
||||
import Connect from './Connect.svelte'
|
||||
</script>
|
||||
|
||||
<div class="d-flex h-full align-items-center flex-column content">
|
||||
<div class="font-size-50 font-weight-bold pt-20 mt-20 root">Watch Together</div>
|
||||
{#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="card d-flex flex-column align-items-center w-300 h-300 justify-content-end">
|
||||
<span class="font-size-80 material-icons d-flex align-items-center h-full">add</span>
|
||||
<button class="btn btn-primary btn-lg mt-10 btn-block" type="button" on:click={() => setState('host')}>Create Lobby</button>
|
||||
</div>
|
||||
<div class="card d-flex flex-column align-items-center w-300 h-300 justify-content-end">
|
||||
<span class="font-size-80 material-icons d-flex align-items-center h-full">group_add</span>
|
||||
<button class="btn btn-primary btn-lg mt-10 btn-block" type="button" on:click={() => setState('guest')}>Join Lobby</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $pending}
|
||||
<Connect bind:state={$state} peer={$pending} {cancel} />
|
||||
{:else}
|
||||
<Lobby bind:state={$state} bind:peers={$peers} {invite} {cleanup} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.font-size-50 {
|
||||
font-size: 5rem;
|
||||
}
|
||||
.font-size-80 {
|
||||
font-size: 8rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -61,3 +61,13 @@ export async function PromiseBatch (task, items, batchSize) {
|
|||
}
|
||||
return results
|
||||
}
|
||||
|
||||
export function generateRandomHexCode (len) {
|
||||
let hexCode = ''
|
||||
|
||||
while (hexCode.length < len) {
|
||||
hexCode += (Math.round(Math.random() * 15)).toString(16)
|
||||
}
|
||||
|
||||
return hexCode
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue