wip: abandoned as capacitor doesn't support transfering messagechannels which are required for callbacks

this is doable by exposing on both ends, but that's too much work for now
This commit is contained in:
ThaUnknown 2024-02-10 22:11:21 +01:00
parent 45b03c068a
commit d47aa0cd23
43 changed files with 509 additions and 432 deletions

View file

@ -15,6 +15,9 @@
"node": true,
"serviceworker": true
},
"rules": {
"n/no-callback-literal": 0
},
"extends": ["plugin:svelte/recommended", "standard"],
"plugins": ["svelte"],
"parser": "@typescript-eslint/parser",

View file

@ -5,8 +5,6 @@ import { App } from '@capacitor/app'
import { Browser } from '@capacitor/browser'
import IPC from './ipc.js'
IPC.on('open', url => Browser.open({ url }))
App.addListener('appUrlOpen', ({ url }) => handleProtocol(url))
// schema: miru://key/value

View file

@ -1,25 +1,52 @@
import { NodeJS } from 'capacitor-nodejs'
import EventEmitter from 'events'
import { NodeJS } from 'capacitor-nodejs'
import { wrap } from 'comlink'
const ready = NodeJS.whenReady()
const main = new EventEmitter()
export default main
main.on('portRequest', async () => {
globalThis.port = {
onmessage: cb => {
NodeJS.addListener('ipc', ({ args }) => cb(args[0]))
function capacitorEndpoint (nep) {
const listeners = new WeakMap()
return {
postMessage: async (...args) => {
await ready
nep.send({ eventName: 'message', args })
},
postMessage: (data, b) => {
NodeJS.send({ eventName: 'ipc', args: [{ data }] })
addEventListener: (_, eh) => {
const l = (data) => {
if ('handleEvent' in eh) {
eh.handleEvent({ data })
} else {
eh({ data })
}
}
const handle = nep.addListener('message', ({ args }) => l(...args))
listeners.set(eh, handle)
},
removeEventListener: (_, eh) => {
const l = listeners.get(eh)
if (!l) return
nep.removeListener(l)
listeners.delete(eh)
}
}
await ready
NodeJS.send({ eventName: 'port-init', args: [localStorage.getItem('settings')] })
main.emit('port')
})
}
export default {
discord: {
on () {},
handleDiscordStatus () {},
showDiscordStatus () {}
},
protocol: new EventEmitter(),
updater: new EventEmitter(),
close () {},
open () {},
doh () {},
angle () {},
dialog () {},
async version () {}
}
export const TorrentClient = wrap(capacitorEndpoint(NodeJS))
const [_platform, arch] = navigator.platform.split(' ')

View file

@ -2,6 +2,7 @@ import TorrentClient from 'common/modules/webtorrent.js'
import { channel } from 'bridge'
import { env } from 'node:process'
import { statfs } from 'fs/promises'
import { expose } from 'comlink'
async function storageQuota (directory) {
const { bsize, bavail } = await statfs(directory)
@ -16,19 +17,34 @@ if (typeof localStorage === 'undefined') {
}
}
channel.on('port-init', data => {
localStorage.setItem('settings', data)
const port = {
onmessage: _ => {},
postMessage: data => {
channel.send('ipc', { data })
// localStorage.setItem('settings', data)
function capacitorEndpoint (nep) {
const listeners = new WeakMap()
return {
postMessage: (...args) => {
nep.send('message', ...args)
},
addEventListener: (_, eh) => {
const l = (data) => {
if ('handleEvent' in eh) {
eh.handleEvent({ data })
} else {
eh({ data })
}
}
nep.on('message', l)
listeners.set(eh, l)
},
removeEventListener: (_, eh) => {
const l = listeners.get(eh)
if (!l) return
nep.removeListener('message', l)
listeners.delete(eh)
}
}
}
channel.on('ipc', a => port.onmessage(a))
channel.emit('port', ({
ports: [port]
}))
})
globalThis.client = new TorrentClient(storageQuota, 'node', { torrentPath: env.TMPDIR })
globalThis.client = new TorrentClient(channel, storageQuota, 'node', { torrentPath: env.TMPDIR })
expose(globalThis.client, capacitorEndpoint(channel))

View file

@ -1,6 +1,7 @@
<script context='module'>
import { setContext } from 'svelte'
import { writable } from 'simple-store-svelte'
import { proxy } from 'comlink'
import { anilistClient } from '@/modules/anilist.js'
import IPC from '@/modules/ipc.js'
@ -10,10 +11,11 @@
view.set(null)
view.set((await anilistClient.searchIDSingle({ id: anime })).data.Media)
}
IPC.on('open-anime', handleAnime)
IPC.on('schedule', () => {
IPC.protocol.on('open-anime', proxy(handleAnime))
IPC.protocol.on('schedule', proxy(() => {
page.set('schedule')
})
}))
</script>
<script>

View file

@ -15,7 +15,7 @@
if (anilistClient.userID) {
$logout = true
} else {
IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://auth
IPC.open('https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token')
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.",
@ -69,7 +69,7 @@
},
{
click: () => {
IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
IPC.open('https://github.com/sponsors/ThaUnknown/')
},
icon: 'favorite',
text: 'Support This App',

View file

@ -16,7 +16,7 @@
if (anilistClient.userID) {
$logout = true
} else {
IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://auth
IPC.open('https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://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.",
@ -70,7 +70,7 @@
},
{
click: () => {
IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
IPC.open('https://github.com/sponsors/ThaUnknown/')
},
icon: 'favorite',
text: 'Support This App',

View file

@ -3,6 +3,8 @@
"baseUrl": "./",
"paths": {
"@/*": ["./*"],
"three": ["./types.d.ts"],
"rxjs": ["./types.d.ts"],
},
"checkJs": true,
"target": "ESNext",
@ -17,5 +19,9 @@
"types": ["./types.d.ts"],
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules", "dist", "build", "git_modules", ".svelte-kit", "**/node_modules", "**/dist", "**/build", "**/git_modules", "**/.svelte-kit", "**/node_modules/*", "**/dist/*", "**/build/*", "**/git_modules/*", "**/.svelte-kit/*"]
"exclude": [
"node_modules", "dist", "build", "git_modules", ".svelte-kit", "public", "android", "@types/three",
"**/node_modules", "**/dist", "**/build", "**/git_modules", "**/.svelte-kit", "**/public", "**/android", "**/@types/three",
"**/node_modules/*", "**/dist/*", "**/build/*", "**/git_modules/*", "**/.svelte-kit/*", "**/public/*", "**/android/*", "**@types/three/*.d.ts"
]
}

View file

@ -1 +1,3 @@
export default window.IPC
export default globalThis.Native
export const TorrentClient = globalThis.Torrent

View file

@ -5,6 +5,7 @@ export default class Parser {
parsed = false
/** @type {Metadata} */
metadata = null
/** @type {import('./webtorrent.js').default} */
client = null
file = null
destroyed = false
@ -19,13 +20,13 @@ export default class Parser {
this.parsed = true
this.destroy()
} else {
this.client.dispatch('tracks', tracks)
this.client.emit('tracks', tracks)
}
})
this.metadata.getChapters().then(chapters => {
if (this.destroyed) return
this.client.dispatch('chapters', chapters)
this.client.emit('chapters', chapters)
})
this.metadata.getAttachments().then(files => {
@ -33,14 +34,14 @@ export default class Parser {
for (const file of files) {
if (fontRx.test(file.filename) || file.mimetype.toLowerCase().includes('font')) {
// this is cursed, but required, as capacitor-node's IPC hangs for 2mins when runnig on 32bit android when sending uint8's
this.client.dispatch('file', { data: JSON.stringify([...file.data]) })
this.client.emit('file', { data: JSON.stringify([...file.data]) })
}
}
})
this.metadata.on('subtitle', (subtitle, trackNumber) => {
if (this.destroyed) return
this.client.dispatch('subtitle', { subtitle, trackNumber })
this.client.emit('subtitle', { subtitle, trackNumber })
})
if (this.file.name.endsWith('.mkv') || this.file.name.endsWith('.webm')) {

View file

@ -35,16 +35,7 @@ async function updatePeerCounts (entries) {
const id = crypto.randomUUID()
const updated = await Promise.race([
new Promise(resolve => {
function check ({ detail }) {
if (detail.id !== id) return
client.removeListener('scrape', check)
resolve(detail.result)
console.log(detail)
}
client.on('scrape', check)
client.send('scrape', { id, infoHashes: entries.map(({ hash }) => hash) })
}),
client.scrape({ id, infoHashes: entries.map(({ hash }) => hash) }),
sleep(5000)
])

View file

@ -1,4 +1,5 @@
import { writable } from 'simple-store-svelte'
import { proxy } from 'comlink'
import { defaults } from './util.js'
import IPC from '@/modules/ipc.js'
export let alToken = localStorage.getItem('ALtoken') || null
@ -45,7 +46,7 @@ window.addEventListener('paste', ({ clipboardData }) => {
}
}
})
IPC.on('altoken', handleToken)
IPC.protocol.on('altoken', proxy(handleToken))
function handleToken (data) {
localStorage.setItem('ALtoken', data)
alToken = data

View file

@ -1,3 +1,4 @@
import { proxy } from 'comlink'
import JASSUB from 'jassub'
import { toTS, subRx, videoRx } from './util.js'
import { settings } from '@/modules/settings.js'
@ -39,14 +40,14 @@ export default class Subtitles {
this.videoFiles = files.filter(file => videoRx.test(file.name))
this.subtitleFiles = []
this.timeout = null
this.handleFile = ({ detail }) => {
this.handleFile = (detail) => {
if (this.selected) {
const uint8 = new Uint8Array(JSON.parse(detail.data))
this.fonts.push(uint8)
this.renderer?.addFont(uint8)
}
}
this.handleSubtitle = ({ detail }) => {
this.handleSubtitle = (detail) => {
const { subtitle, trackNumber } = detail
if (this.selected) {
const string = JSON.stringify(subtitle)
@ -59,7 +60,7 @@ export default class Subtitles {
}
}
this.handleTracks = ({ detail }) => {
this.handleTracks = (detail) => {
if (this.selected) {
for (const track of detail) {
if (!this.tracks[track.number]) {
@ -110,14 +111,14 @@ export default class Subtitles {
if (subRx.test(file.name)) this.addSingleSubtitleFile(file)
}
}
this.handleSubtitleFile = ({ detail }) => {
this.handleSubtitleFile = (detail) => {
this.addSingleSubtitleFile(new File([detail.data], detail.name))
}
client.on('tracks', this.handleTracks)
client.on('subtitle', this.handleSubtitle)
client.on('file', this.handleFile)
client.on('subtitleFile', this.handleSubtitleFile)
client.on('tracks', proxy(this.handleTracks))
client.on('subtitle', proxy(this.handleSubtitle))
client.on('file', proxy(this.handleFile))
client.on('subtitleFile', proxy(this.handleSubtitleFile))
clipboard.on('text', this.handleClipboardText)
clipboard.on('files', this.handleClipboardFiles)
}

View file

@ -2,68 +2,52 @@ import { files, media } from '../views/Player/MediaHandler.svelte'
import { page } from '@/App.svelte'
import { toast } from 'svelte-sonner'
import clipboard from './clipboard.js'
import IPC from '@/modules/ipc.js'
import 'browser-event-target-emitter'
import { TorrentClient } from '@/modules/ipc.js'
import { proxy } from 'comlink'
const torrentRx = /(^magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent$){1}/i
class TorrentWorker extends EventTarget {
constructor () {
super()
this.ready = new Promise(resolve => {
IPC.once('port', () => {
this.port = window.port
this.port.onmessage(this.handleMessage.bind(this))
resolve()
})
IPC.emit('portRequest')
})
clipboard.on('text', ({ detail }) => {
for (const { text } of detail) {
if (torrentRx.exec(text)) {
media.set(null)
add(text)
}
}
})
clipboard.on('files', async ({ detail }) => {
for (const file of detail) {
if (file.name.endsWith('.torrent')) {
media.set(null)
add(new Uint8Array(await file.arrayBuffer()))
}
}
})
clipboard.on('text', ({ detail }) => {
for (const { text } of detail) {
if (torrentRx.exec(text)) {
media.set(null)
add(text)
}
}
handleMessage ({ data }) {
this.emit(data.type, data.data)
})
clipboard.on('files', async ({ detail }) => {
for (const file of detail) {
if (file.name.endsWith('.torrent')) {
media.set(null)
add(new Uint8Array(await file.arrayBuffer()))
}
}
async send (type, data, transfer) {
await this.ready
console.info('Torrent: sending message', { type, data })
this.port.postMessage({ type, data }, transfer)
}
}
export const client = new TorrentWorker()
client.send('load', localStorage.getItem('torrent'))
client.on('files', ({ detail }) => {
files.set(detail)
})
client.on('error', ({ detail }) => {
globalThis.ddd = TorrentClient
/** @type {import('comlink').Remote<import('@/modules/webtorrent.js').default>} */
export const client = TorrentClient
queueMicrotask(() => client.loadLastTorrent(localStorage.getItem('torrent')))
window.dd = client
window.aa = proxy
client.on('files', proxy((detail) => {
files.set(detail)
}))
client.on('error', proxy((detail) => {
console.error(detail)
toast.error('Torrent Error', { description: detail.message || detail })
})
}))
client.on('warn', ({ detail }) => {
client.on('warn', proxy((detail) => {
console.error(detail)
toast.warning('Torrent Warning', { description: detail.message || detail })
})
}))
export async function add (torrentID, hide) {
if (torrentID) {
@ -71,6 +55,6 @@ export async function add (torrentID, hide) {
localStorage.setItem('torrent', torrentID)
files.set([])
if (!hide) page.set('player')
client.send('torrent', torrentID)
client.addTorrent(torrentID)
}
}

View file

@ -1,3 +1,4 @@
import { EventEmitter } from 'node:events'
import WebTorrent from 'webtorrent'
import HTTPTracker from 'bittorrent-tracker/lib/client/http-tracker.js'
import { hex2bin, arr2hex, text2arr } from 'uint8-util'
@ -30,12 +31,13 @@ try {
storedSettings = JSON.parse(localStorage.getItem('settings')) || {}
} catch (error) {}
export default class TorrentClient extends WebTorrent {
export default class TorrentClient {
static excludedErrorMessages = ['WebSocket', 'User-Initiated Abort, reason=', 'Connection failed.']
constructor (ipc, storageQuota, serverMode, settingOverrides = {}, controller) {
emitter = new EventEmitter()
client
constructor (storageQuota, serverMode, settingOverrides = {}, controller) {
const settings = { ...defaults, ...storedSettings, ...settingOverrides }
super({
this.client = new WebTorrent({
dht: !settings.torrentDHT,
maxConns: settings.maxConns,
downloadLimit: settings.torrentSpeed * 1048576 || 0,
@ -43,18 +45,7 @@ export default class TorrentClient extends WebTorrent {
torrentPort: settings.torrentPort || 0,
dhtPort: settings.dhtPort || 0
})
this._ready = new Promise(resolve => {
ipc.on('port', ({ ports }) => {
this.message = ports[0].postMessage.bind(ports[0])
ports[0].onmessage = ({ data }) => {
if (data.type === 'load') this.loadLastTorrent(data.data)
if (data.type === 'destroy') this.destroy()
this.handleMessage({ data })
}
resolve()
})
ipc.on('destroy', this.destroy.bind(this))
})
this.settings = settings
this.serverMode = serverMode
@ -64,19 +55,17 @@ export default class TorrentClient extends WebTorrent {
this.parsed = false
setInterval(() => {
this.dispatch('stats', {
numPeers: (this.torrents.length && this.torrents[0].numPeers) || 0,
uploadSpeed: (this.torrents.length && this.torrents[0].uploadSpeed) || 0,
downloadSpeed: (this.torrents.length && this.torrents[0].downloadSpeed) || 0
this.emit('stats', {
numPeers: (this.client.torrents.length && this.client.torrents[0].numPeers) || 0,
uploadSpeed: (this.client.torrents.length && this.client.torrents[0].uploadSpeed) || 0,
downloadSpeed: (this.client.torrents.length && this.client.torrents[0].downloadSpeed) || 0,
progress: (this.client.torrents[0]?.pieces && this.current?.progress) || 0
})
}, 200)
setInterval(() => {
if (this.torrents[0]?.pieces) this.dispatch('progress', this.current?.progress)
}, 2000)
this.on('torrent', this.handleTorrent.bind(this))
}, 1000)
this.client.on('torrent', this.handleTorrent.bind(this))
const createServer = controller => {
this.server = this.createServer({ controller }, serverMode)
this.server = this.client.createServer({ controller }, serverMode)
this.server.listen(0, () => {})
}
@ -91,7 +80,24 @@ export default class TorrentClient extends WebTorrent {
}
process.on('uncaughtException', this.dispatchError.bind(this))
this.on('error', this.dispatchError.bind(this))
this.client.on('error', this.dispatchError.bind(this))
}
emit (event, ...args) {
if (!this.events[event]?.length) return
for (const cb of this.events[event]) {
cb(...args)
}
}
off (event) {
delete this.events[event]
}
events = {}
on (event, cb) {
(this.events[event] ||= []).push(cb)
}
loadLastTorrent (t) {
@ -123,19 +129,19 @@ export default class TorrentClient extends WebTorrent {
for (const file of torrent.files) {
file.deselect()
}
this.dispatch('warn', 'Detected Large Torrent! To Conserve Drive Space Files Will Be Downloaded Selectively Instead Of Downloading The Entire Torrent.')
this.emit('warn', 'Detected Large Torrent! To Conserve Drive Space Files Will Be Downloaded Selectively Instead Of Downloading The Entire Torrent.')
}
this.dispatch('files', files)
this.dispatch('magnet', { magnet: torrent.magnetURI, hash: torrent.infoHash })
this.emit('files', files)
this.emit('magnet', { magnet: torrent.magnetURI, hash: torrent.infoHash })
localStorage.setItem('torrent', JSON.stringify([...torrent.torrentFile]))
if (torrent.length > await this.storageQuota(torrent.path)) {
this.dispatch('error', 'Torrent Too Big! This Torrent Exceeds The Selected Drive\'s Available Space. Change Download Location In Torrent Settings To A Drive With More Space And Restart The App!')
this.emit('error', 'Torrent Too Big! This Torrent Exceeds The Selected Drive\'s Available Space. Change Download Location In Torrent Settings To A Drive With More Space And Restart The App!')
}
}
async findFontFiles (targetFile) {
const files = this.torrents[0].files
const files = this.client.torrents[0].files
const fontFiles = files.filter(file => fontRx.test(file.name))
const map = {}
@ -151,12 +157,12 @@ export default class TorrentClient extends WebTorrent {
for (const file of Object.values(map)) {
const data = await file.arrayBuffer()
if (targetFile !== this.current) return
this.dispatch('file', { data: new Uint8Array(data) }, [data])
this.emit('file', { data: new Uint8Array(data) }, [data])
}
}
async findSubtitleFiles (targetFile) {
const files = this.torrents[0].files
const files = this.client.torrents[0].files
const videoFiles = files.filter(file => videoRx.test(file.name))
const videoName = targetFile.name.substring(0, targetFile.name.lastIndexOf('.')) || targetFile.name
// array of subtitle files that match video name, or all subtitle files when only 1 vid file
@ -166,22 +172,24 @@ export default class TorrentClient extends WebTorrent {
for (const file of subfiles) {
const data = await file.arrayBuffer()
if (targetFile !== this.current) return
this.dispatch('subtitleFile', { name: file.name, data: new Uint8Array(data) }, [data])
this.emit('subtitleFile', { name: file.name, data: new Uint8Array(data) }, [data])
}
}
_scrape ({ id, infoHashes }) {
this.trackers.cat._request(this.trackers.cat.scrapeUrl, { info_hash: infoHashes.map(infoHash => hex2bin(infoHash)) }, (err, data) => {
if (err) {
this.dispatch('error', err)
return this.dispatch('scrape', { id, result: [] })
}
const { files } = data
const result = []
for (const [key, data] of Object.entries(files || {})) {
result.push({ hash: key.length !== 40 ? arr2hex(text2arr(key)) : key, ...data })
}
this.dispatch('scrape', { id, result })
scrape ({ id, infoHashes }) {
return new Promise((resolve, reject) => {
this.trackers.cat._request(this.trackers.cat.scrapeUrl, { info_hash: infoHashes.map(infoHash => hex2bin(infoHash)) }, (err, data) => {
if (err) {
this.emit('warn', err)
return resolve({ id, result: [] })
}
const { files } = data
const result = []
for (const [key, data] of Object.entries(files || {})) {
result.push({ hash: key.length !== 40 ? arr2hex(text2arr(key)) : key, ...data })
}
resolve({ id, result })
})
})
}
@ -191,18 +199,18 @@ export default class TorrentClient extends WebTorrent {
for (const exclude of TorrentClient.excludedErrorMessages) {
if (e.message?.startsWith(exclude)) return
}
this.dispatch('error', JSON.stringify(e?.message || e))
this.emit('error', JSON.stringify(e?.message || e))
}
async addTorrent (data, skipVerify = false) {
const existing = await this.get(data)
const existing = await this.client.get(data)
if (existing) {
if (existing.ready) this.handleTorrent(existing)
return
}
localStorage.setItem('lastFinished', false)
if (this.torrents.length) await this.remove(this.torrents[0])
const torrent = await this.add(data, {
if (this.client.torrents.length) await this.client.remove(this.client.torrents[0])
const torrent = await this.client.add(data, {
private: this.settings.torrentPeX,
path: this.settings.torrentPath,
destroyStoreOnDestroy: !this.settings.torrentPersist,
@ -215,44 +223,25 @@ export default class TorrentClient extends WebTorrent {
})
}
async handleMessage ({ data }) {
switch (data.type) {
case 'current': {
if (data.data) {
const torrent = await this.get(data.data.infoHash)
const found = torrent?.files.find(file => file.path === data.data.path)
if (!found) return
if (this.current) {
this.current.removeAllListeners('stream')
}
this.parser?.destroy()
found.select()
this.current = found
this.parser = new Parser(this, found)
this.findSubtitleFiles(found)
this.findFontFiles(found)
}
break
}
case 'scrape': {
this._scrape(data.data)
break
}
case 'torrent': {
this.addTorrent(data.data)
break
}
async setCurrent (file) {
const torrent = await this.client.get(file.infoHash)
const found = torrent?.files.find(f => f.path === file.path)
if (!found) return
if (this.current) {
this.current.removeAllListeners('stream')
}
this.parser?.destroy()
found.select()
this.current = found
this.parser = new Parser(this, found)
this.findSubtitleFiles(found)
this.findFontFiles(found)
}
async dispatch (type, data, transfer) {
await this._ready
this.message?.({ type, data }, transfer)
}
destroy () {
this.parser?.destroy()
this.server.close()
super.destroy()
this.client.destroy()
}
}

View file

@ -27,10 +27,10 @@
<div class='font-size-16'>Most features of Miru will not function correctly without being able to connect to an API.</div>
<div class='font-size-16'>If you enable a VPN a restart might be required for it to take effect.</div>
<!-- eslint-disable-next-line svelte/valid-compile -->
<div class='font-size-16'>Visit <a class='text-primary pointer' use:click={() => { IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>this guide</a> for a tutorial on how to bypass ISP blocks.</div>
<div class='font-size-16'>Visit <a class='text-primary pointer' use:click={() => { IPC.open('https://thewiki.moe/tutorials/unblock/') }}>this guide</a> for a tutorial on how to bypass ISP blocks.</div>
<div class='d-flex w-full mt-20 pt-20'>
<button class='btn ml-auto mr-5' type='button' use:click={() => { block = false }}>I Understand</button>
<button class='btn btn-primary mr-5' type='button' use:click={() => { IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>Open Guide</button>
<button class='btn btn-primary mr-5' type='button' use:click={() => { IPC.open('https://thewiki.moe/tutorials/unblock/') }}>Open Guide</button>
<button class='btn btn-primary' type='button' use:click={testConnection}>Reconnect</button>
</div>
</div>

View file

@ -240,7 +240,7 @@
}
]
}
IPC.emit('discord', { activity })
IPC.discord.handleDiscordStatus({ activity })
}
state.subscribe(() => {
setDiscordRPC()

View file

@ -17,6 +17,7 @@
import Keybinds, { loadWithDefaults, condition } from 'svelte-keybinds'
import { SUPPORTS } from '@/modules/support.js'
import 'rvfc-polyfill'
import { proxy } from 'comlink'
const emit = createEventDispatcher()
@ -171,7 +172,7 @@
current = file
emit('current', current)
src = file.url
client.send('current', file)
client.setCurrent(file)
subs = new Subtitles(video, files, current, handleHeaders)
video.load()
await loadAnimeProgress()
@ -718,14 +719,10 @@
return 0
}
let buffer = 0
client.on('progress', ({ detail }) => {
buffer = detail * 100
})
let chapters = []
client.on('chapters', ({ detail }) => {
if (detail.length) chapters = detail
})
client.on('chapters', proxy(data => {
if (data.length) chapters = data
}))
async function findChapters () {
if (!chapters.length && current.media.media) {
chapters = await getChaptersAniSkip(current, safeduration)
@ -891,11 +888,13 @@
}
}
const torrent = {}
client.on('stats', updateStats)
function updateStats ({ detail }) {
client.on('stats', proxy(updateStats))
function updateStats (detail) {
torrent.peers = detail.numPeers || 0
torrent.up = detail.uploadSpeed || 0
torrent.down = detail.downloadSpeed || 0
buffer = detail.progress * 100
}
function checkError ({ target }) {
// video playback failed - show a message saying why

View file

@ -21,7 +21,7 @@
location.reload()
}
function checkUpdate () {
IPC.emit('update')
IPC.updater.checkForUpdates()
}
setInterval(checkUpdate, 1200000)
</script>

View file

@ -75,18 +75,18 @@
<button type='button' use:click={() => { homeSections[homeSections.length] = 'Trending Now' }} class='btn btn-primary'>Add Section</button>
<style>
.ghost {
margin-bottom: 10px;
pointer-events: none;
z-index: 9999;
position: absolute !important;
}
.ghost {
margin-bottom: 10px;
pointer-events: none;
z-index: 9999;
position: absolute !important;
}
.tp {
opacity: 0;
}
.tp {
opacity: 0;
}
.grab{
cursor: grab;
}
.grab{
cursor: grab;
}
</style>

View file

@ -6,7 +6,7 @@
import SettingCard from './SettingCard.svelte'
import { SUPPORTS } from '@/modules/support.js'
function updateAngle () {
IPC.emit('angle', settings.value.angle)
IPC.angle(settings.value.angle)
}
export let settings
</script>

View file

@ -1,10 +1,11 @@
<script context='module'>
import { toast } from 'svelte-sonner'
import { proxy } from 'comlink'
import { click } from '@/modules/click.js'
import { settings } from '@/modules/settings.js'
import IPC from '@/modules/ipc.js'
if (settings.value.enableDoH) IPC.emit('doh', settings.value.doHURL)
if (settings.value.enableDoH) IPC.doh(settings.value.doHURL)
export const platformMap = {
aix: 'Aix',
darwin: 'MacOS',
@ -17,35 +18,33 @@
win32: 'Windows'
}
let version = '1.0.0'
IPC.on('version', data => (version = data))
IPC.emit('version')
IPC.version().then(data => (version = data))
let wasUpdated = false
IPC.on('update-available', () => {
IPC.updater.on('update-available', proxy(() => {
if (!wasUpdated) {
wasUpdated = true
toast('Auto Updater', {
description: 'A new version of Miru is available. Downloading!'
})
}
})
IPC.on('update-downloaded', () => {
}))
IPC.updater.on('update-downloaded', proxy(() => {
toast.success('Auto Updater', {
description: 'A new version of Miru has downloaded. You can restart to update!'
})
})
}))
const changeLog = (async () => {
const res = await fetch('https://api.github.com/repos/ThaUnknown/miru/releases')
const json = await res.json()
return json.map(({ body, tag_name: version, published_at: date, assets }) => ({ body, version, date, assets }))
})()
IPC.emit('show-discord-status', settings.value.showDetailsInRPC)
IPC.discord.showDiscordStatus(settings.value.showDetailsInRPC)
</script>
<script>
import { Tabs, TabLabel, Tab } from '@/components/Tabination.js'
import { onDestroy } from 'svelte'
import PlayerSettings from './PlayerSettings.svelte'
import TorrentSettings from './TorrentSettings.svelte'
import InterfaceSettings from './InterfaceSettings.svelte'
@ -74,14 +73,7 @@
icon: 'description'
}
}
function pathListener (data) {
$settings.torrentPath = data
}
onDestroy(() => {
IPC.off('path', pathListener)
})
$: IPC.emit('show-discord-status', $settings.showDetailsInRPC)
IPC.on('path', pathListener)
$: IPC.discord.showDiscordStatus($settings.showDetailsInRPC)
</script>
<Tabs>
@ -96,7 +88,7 @@
</div>
</TabLabel>
{/each}
<div class='pointer my-5 rounded' tabindex='0' role='button' use:click={() => IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')}>
<div class='pointer my-5 rounded' tabindex='0' role='button' use:click={() => IPC.open('https://github.com/sponsors/ThaUnknown/')}>
<div class='px-20 py-10 d-flex'>
<span class='material-symbols-outlined font-size-24 pr-10 d-inline-flex justify-content-center align-items-center'>favorite</span>
<div class='font-weight-bold font-size-16'>Donate</div>

View file

@ -7,7 +7,9 @@
export let settings
function handleFolder () {
IPC.emit('dialog')
IPC.dialog().then(data => {
settings.torrentPath = data
})
}
</script>

View file

@ -66,9 +66,6 @@
duration: '5000'
})
}
function openInBrowser (url) {
IPC.emit('open', url)
}
function getPlayText (media) {
if (media.mediaListEntry) {
const { status, progress } = media.mediaListEntry
@ -136,7 +133,7 @@
</button>
{/if}
<div class='d-flex mb-5 w-full'>
<button class='btn flex-fill font-weight-bold font-size-16 d-flex align-items-center' use:click={() => { openInBrowser(`https://anilist.co/anime/${media.id}`) }}>
<button class='btn flex-fill font-weight-bold font-size-16 d-flex align-items-center' use:click={() => { IPC.open(`https://anilist.co/anime/${media.id}`) }}>
<span class='material-symbols-outlined mr-15 font-size-18 w-30'> open_in_new </span>
Open
</button>

View file

@ -23,7 +23,7 @@
<img src={friend.user.avatar.medium} alt='avatar' class='w-50 h-50 img-fluid rounded cover-img' />
<span class='my-0 pl-20 mr-auto text-truncate'>{friend.user.name}</span>
<span class='my-0 px-10 text-capitalize'>{friend.status.toLowerCase()}</span>
<span class='material-symbols-outlined pointer text-primary font-size-18' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
<span class='material-symbols-outlined pointer text-primary font-size-18' use:click={() => IPC.open('https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
</div>
{/each}
</div>

View file

@ -63,9 +63,6 @@
duration: 5000
})
}
function openInBrowser (url) {
IPC.emit('open', url)
}
let episodeOrder = true
</script>
@ -134,7 +131,7 @@
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
share
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => openInBrowser(`https://anilist.co/anime/${media.id}`)}>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => IPC.open(`https://anilist.co/anime/${media.id}`)}>
open_in_new
</button>
</div>

View file

@ -21,7 +21,7 @@
{/if}
<h4 class='my-0 pl-20 mr-auto'>{peer.user?.name || 'Anonymous'}</h4>
{#if peer.user?.name}
<span class='material-symbols-outlined pointer text-primary' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}> open_in_new </span>
<span class='material-symbols-outlined pointer text-primary' use:click={() => IPC.open('https://anilist.co/user/' + peer.user?.name)}> open_in_new </span>
{/if}
<!-- {#if state === 'host'}
<span class='material-symbols-outlined ml-15 pointer text-danger' use:click={() => peer.peer.pc.close()}> logout </span>

View file

@ -6,6 +6,7 @@
import { toast } from 'svelte-sonner'
import { page } from '@/App.svelte'
import P2PT from 'p2pt'
import { proxy } from 'comlink'
import { click } from '@/modules/click.js'
import IPC from '@/modules/ipc.js'
import 'browser-event-target-emitter'
@ -108,13 +109,13 @@
}
})
client.on('magnet', ({ detail }) => {
client.on('magnet', proxy((detail) => {
if (detail.hash !== playerState.hash) {
playerState.hash = detail.hash
playerState.index = 0
emit('torrent', detail)
}
})
}))
function emit (type, data) {
if (p2pt) {
@ -131,10 +132,10 @@
index: 0
}
IPC.on('w2glink', link => {
IPC.protocol.on('w2glink', proxy(link => {
joinLobby(link)
page.set('watchtogether')
})
}))
function cleanup () {
state.set(false)

View file

@ -81,7 +81,10 @@
},
"win": {
"artifactName": "${os}-${name}-${version}.${ext}",
"target": ["nsis", "portable"]
"target": [
"nsis",
"portable"
]
},
"linux": {
"artifactName": "${os}-${name}-${version}.${ext}",

View file

@ -1,5 +1,6 @@
import { statfs } from 'node:fs/promises'
import { ipcRenderer } from 'electron'
import { statfs } from 'fs/promises'
import { expose } from 'comlink'
import TorrentClient from 'common/modules/webtorrent.js'
@ -8,4 +9,7 @@ async function storageQuota (directory) {
return bsize * bavail
}
globalThis.client = new TorrentClient(ipcRenderer, storageQuota, 'node')
globalThis.client = new TorrentClient(storageQuota, 'node')
ipcRenderer.on('port', ({ ports }) => {
expose(globalThis.client, ports[0])
})

View file

@ -1,5 +1,4 @@
import { Client } from 'discord-rpc'
import { ipcMain } from 'electron'
import { debounce } from '@/modules/util.js'
export default class {
@ -35,16 +34,6 @@ export default class {
transport: 'ipc'
})
ipcMain.on('show-discord-status', (event, data) => {
this.allowDiscordDetails = data
this.debouncedDiscordRPC(this.allowDiscordDetails ? this.cachedPresence : undefined)
})
ipcMain.on('discord', (event, data) => {
this.cachedPresence = data
this.debouncedDiscordRPC(this.allowDiscordDetails ? this.cachedPresence : undefined)
})
this.discord.on('ready', async () => {
this.setDiscordRPC(this.cachedPresence || this.defaultStatus)
this.discord.subscribe('ACTIVITY_JOIN_REQUEST')
@ -61,6 +50,16 @@ export default class {
this.debouncedDiscordRPC = debounce(status => this.setDiscordRPC(status), 4500)
}
handleDiscordStatus (data) {
this.cachedPresence = data
this.debouncedDiscordRPC(this.allowDiscordDetails && this.cachedPresence)
}
showDiscordStatus (data) {
this.allowDiscordDetails = data
this.debouncedDiscordRPC(this.allowDiscordDetails & this.cachedPresence)
}
loginRPC () {
this.discord.login({ clientId: '954855428355915797' }).catch(() => {
setTimeout(() => this.loginRPC(), 5000).unref()

View file

@ -1,19 +1,46 @@
/* eslint-disable no-new */
import { app, BrowserWindow, shell, ipcMain, dialog, MessageChannelMain } from 'electron'
import { app, BrowserWindow, shell, ipcMain, dialog } from 'electron'
import path from 'path'
import Discord from './discord.js'
import Updater from './updater.js'
import Protocol from './protocol.js'
import { development } from './util.js'
import { expose } from 'comlink'
import { Util, development } from './util.js'
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
let webtorrentWindow
function electronEndpoint (nep) {
const listeners = new WeakMap()
return {
postMessage: (...args) => {
nep.postMessage(...args)
},
addEventListener: (_, eh) => {
const l = data => {
if (data.data?.argumentList) {
for (const arg of data.data.argumentList) {
if (arg.type === 'HANDLER' && arg.name === 'proxy') arg.value = electronEndpoint(data.ports[0])
}
}
if ('handleEvent' in eh) {
eh.handleEvent(data)
} else {
eh(data)
}
}
nep.on('message', l)
listeners.set(eh, l)
},
removeEventListener: (_, eh) => {
const l = listeners.get(eh)
if (!l) {
return
}
nep.off('message', l)
listeners.delete(eh)
},
start: nep.start && nep.start.bind(nep)
}
}
function createWindow () {
// Create the browser window.
webtorrentWindow = new BrowserWindow({
const webtorrentWindow = new BrowserWindow({
show: development,
webPreferences: {
nodeIntegration: true,
@ -21,7 +48,7 @@ function createWindow () {
backgroundThrottling: false
}
})
mainWindow = new BrowserWindow({
let mainWindow = new BrowserWindow({
width: 1600,
height: 900,
frame: process.platform === 'darwin', // Only keep the native frame on Mac
@ -36,14 +63,22 @@ function createWindow () {
webPreferences: {
enableBlinkFeatures: 'FontAccess, AudioVideoTracks',
backgroundThrottling: false,
contextIsolation: false,
preload: path.join(__dirname, '/preload.js')
},
icon: path.join(__dirname, '/logo.ico'),
show: false
})
new Discord(mainWindow)
new Protocol(mainWindow)
new Updater(mainWindow)
ipcMain.on('mainPort', ({ ports }) => {
expose(new Util(mainWindow), electronEndpoint(ports[0]))
})
ipcMain.on('torrentPort', async ({ ports }) => {
await torrentLoad
webtorrentWindow.webContents.postMessage('port', null, [ports[0]])
})
mainWindow.setMenuBarVisibility(false)
mainWindow.webContents.session.webRequest.onHeadersReceived(({ responseHeaders }, fn) => {
@ -72,14 +107,6 @@ function createWindow () {
app.quit()
})
ipcMain.on('close', () => {
mainWindow = null
try {
webtorrentWindow.webContents.postMessage('destroy', null)
} catch (e) {}
app.quit()
})
let crashcount = 0
mainWindow.webContents.on('render-process-gone', (e, { reason }) => {
if (reason === 'crashed') {
@ -100,16 +127,10 @@ function createWindow () {
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
ipcMain.on('portRequest', async ({ sender }) => {
const { port1, port2 } = new MessageChannelMain()
await torrentLoad
webtorrentWindow.webContents.postMessage('port', null, [port1])
sender.postMessage('port', null, [port2])
})
}
app.on('ready', createWindow)
app.on('activate', () => {
if (mainWindow === null) createWindow()
if (!BrowserWindow.getAllWindows().length) createWindow()
})

View file

@ -1,6 +1,6 @@
import path from 'node:path'
import { app, protocol, shell, ipcMain } from 'electron'
import { development } from './util.js'
import path from 'path'
if (process.defaultApp) {
if (process.argv.length >= 2) {
@ -10,27 +10,42 @@ if (process.defaultApp) {
app.setAsDefaultProtocolClient('miru')
}
export default class {
export default class Protocol {
// schema: miru://key/value
protocolMap = {
auth: token => this.sendToken(token),
anime: id => this.window.webContents.send('open-anime', id),
w2g: link => this.window.webContents.send('w2glink', link),
schedule: () => this.window.webContents.send('schedule'),
anime: id => this.emit('open-anime', id),
w2g: link => this.emit('w2glink', link),
schedule: () => this.emit('schedule'),
donate: () => shell.openExternal('https://github.com/sponsors/ThaUnknown/')
}
protocolRx = /miru:\/\/([a-z0-9]+)\/(.*)/i
emit (event, ...args) {
if (!this.events[event]?.length) return
for (const cb of this.events[event]) {
cb(...args)
}
}
off (event) {
delete this.events[event]
}
events = {}
on (event, cb) {
(this.events[event] ||= []).push(cb)
}
/**
* @param {import('electron').BrowserWindow} window
*/
constructor (window) {
this.window = window
protocol.registerHttpProtocol('miru', (req, cb) => {
const token = req.url.slice(7)
this.window.loadURL(development ? 'http://localhost:5000/app.html' + token : `file://${path.join(__dirname, '/app.html')}${token}`)
window.loadURL(development ? 'http://localhost:5000/app.html' + token : `file://${path.join(__dirname, '/app.html')}${token}`)
})
app.on('open-url', (event, url) => {
@ -48,9 +63,9 @@ export default class {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (this.window) {
if (this.window.isMinimized()) this.window.restore()
this.window.focus()
if (window) {
if (window.isMinimized()) window.restore()
window.focus()
}
// There's probably a better way to do this instead of a for loop and split[1][0]
// but for now it works as a way to fix multiple OS's commandLine differences
@ -64,7 +79,7 @@ export default class {
let token = line.split('access_token=')[1].split('&token_type')[0]
if (token) {
if (token.endsWith('/')) token = token.slice(0, -1)
this.window.webContents.send('altoken', token)
this.emit('altoken', token)
}
}

View file

@ -1,24 +1,38 @@
import log from 'electron-log'
import { autoUpdater } from 'electron-updater'
import { ipcMain } from 'electron'
log.transports.file.level = 'info'
autoUpdater.logger = log
ipcMain.on('update', () => {
autoUpdater.checkForUpdatesAndNotify()
})
autoUpdater.checkForUpdatesAndNotify()
export default class {
/**
* @param {import('electron').BrowserWindow} window
*/
constructor (window) {
export default class Updater {
emit (event, ...args) {
if (!this.events[event]?.length) return
for (const cb of this.events[event]) {
cb(...args)
}
}
off (event) {
delete this.events[event]
}
events = {}
on (event, cb) {
(this.events[event] ||= []).push(cb)
}
constructor () {
autoUpdater.on('update-available', () => {
window.webContents.send('update-available', true)
this.emit('update-available', true)
})
autoUpdater.on('update-downloaded', () => {
window.webContents.send('update-downloaded', true)
this.emit('update-downloaded', true)
})
}
checkForUpdates () {
autoUpdater.checkForUpdatesAndNotify()
}
}

View file

@ -1,8 +1,75 @@
import { app, ipcMain, shell, dialog } from 'electron'
import { app, dialog, shell } from 'electron'
import store from './store.js'
import Discord from './discord.js'
import Protocol from './protocol.js'
import Updater from './updater.js'
export const development = process.env.NODE_ENV?.trim() === 'development'
export class Util {
mainWindow
discord
protocol
updater
/**
* @param {import('electron').BrowserWindow} mainWindow
*/
constructor (mainWindow) {
this.mainWindow = mainWindow
this.discord = new Discord(mainWindow)
this.protocol = new Protocol(mainWindow)
this.updater = new Updater()
}
close () {
this.mainWindow = null
// try {
// this.webtorrentWindow.webContents.postMessage('destroy', null)
// } catch (e) {}
app.quit()
}
open (url) {
shell.openExternal(url)
}
doh (dns) {
try {
const url = new URL(dns)
app.configureHostResolver({
secureDnsMode: 'secure',
secureDnsServers: [url.toString()]
})
} catch (e) {}
}
angle (angle) {
return store.set('angle', angle)
}
async dialog () {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (filePaths.length) {
let path = filePaths[0]
if (!(path.endsWith('\\') || path.endsWith('/'))) {
if (path.indexOf('\\') !== -1) {
path += '\\'
} else if (path.indexOf('/') !== -1) {
path += '/'
}
}
return path
}
}
version () {
return app.getVersion()
}
}
const flags = [
['disable-gpu-sandbox'],
['disable-direct-composition-video-overlays'],
@ -27,50 +94,6 @@ app.commandLine.appendSwitch('use-angle', store.get('angle') || 'default')
if (!app.requestSingleInstanceLock()) app.quit()
ipcMain.on('open', (event, url) => {
shell.openExternal(url)
})
ipcMain.on('doh', (event, dns) => {
try {
const url = new URL(dns)
app.configureHostResolver({
secureDnsMode: 'secure',
secureDnsServers: [url.toString()]
})
} catch (e) {}
})
ipcMain.on('angle', (e, data) => {
store.set('angle', data)
})
ipcMain.on('close', () => {
app.quit()
})
ipcMain.on('dialog', async (event, data) => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (filePaths.length) {
let path = filePaths[0]
if (!(path.endsWith('\\') || path.endsWith('/'))) {
if (path.indexOf('\\') !== -1) {
path += '\\'
} else if (path.indexOf('/') !== -1) {
path += '/'
}
}
event.sender.send('path', path)
}
})
ipcMain.on('version', (event) => {
event.sender.send('version', app.getVersion()) // fucking stupid
})
app.setJumpList?.([
{
name: 'Frequent',

View file

@ -1,32 +1,22 @@
/* eslint n/no-callback-literal: 0 */
import { contextBridge, ipcRenderer } from 'electron'
import { ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('IPC', {
emit: (event, data) => {
ipcRenderer.send(event, data)
},
on: (event, callback) => {
ipcRenderer.on(event, (event, ...args) => callback(...args))
},
once: (event, callback) => {
ipcRenderer.once(event, (event, ...args) => callback(...args))
},
off: (event) => {
ipcRenderer.removeAllListeners(event)
const mainChannel = new MessageChannel()
globalThis.Native = mainChannel.port1
ipcRenderer.postMessage('mainPort', null, [mainChannel.port2])
const d = mainChannel.port1.postMessage.bind(mainChannel.port1)
mainChannel.port1.postMessage = (...args) => {
if (args[0]?.argumentList) {
for (const arg of args[0].argumentList) {
if (arg.type === 'HANDLER' && arg.name === 'proxy') delete arg.value
}
}
})
contextBridge.exposeInMainWorld('version', {
d(...args)
}
const torrentChannel = new MessageChannel()
globalThis.Torrent = torrentChannel.port1
ipcRenderer.postMessage('torrentPort', null, [torrentChannel.port2])
globalThis.version = {
arch: process.arch,
platform: process.platform
})
ipcRenderer.once('port', ({ ports }) => {
contextBridge.exposeInMainWorld('port', {
onmessage: (cb) => {
ports[0].onmessage = ({ type, data }) => cb({ type, data })
},
postMessage: (a, b) => {
ports[0].postMessage(a, b)
}
})
})
}

View file

@ -0,0 +1,5 @@
import { wrap } from 'comlink'
export default wrap(globalThis.Native)
export const TorrentClient = wrap(globalThis.Torrent)

View file

@ -5,6 +5,10 @@ const mode = process.env.NODE_ENV?.trim() || 'development'
const commonConfig = require('common/webpack.config.cjs')
const alias = {
'@/modules/ipc.js': join(__dirname, 'src', 'renderer', 'ipc.js')
}
module.exports = [
{
devtool: 'source-map',
@ -40,7 +44,7 @@ module.exports = [
port: 5000
}
},
commonConfig(__dirname),
commonConfig(__dirname, alias),
{
devtool: 'source-map',
entry: join(__dirname, 'src', 'preload', 'preload.js'),

View file

@ -13,8 +13,14 @@
"skipLibCheck": true,
"paths": {
"@/*": ["./common/*"],
"three": ["./types.d.ts"],
"rxjs": ["./types.d.ts"],
},
"types": ["@cloudflare/workers-types", "./types.d.ts"],
},
"exclude": ["node_modules", "dist", "build", "git_modules", ".svelte-kit", "**/node_modules", "**/dist", "**/build", "**/git_modules", "**/.svelte-kit", "**/node_modules/*", "**/dist/*", "**/build/*", "**/git_modules/*", "**/.svelte-kit/*"]
"exclude": [
"node_modules", "dist", "build", "git_modules", ".svelte-kit", "public", "android", "@types/three",
"**/node_modules", "**/dist", "**/build", "**/git_modules", "**/.svelte-kit", "**/public", "**/android", "**/@types/three",
"**/node_modules/*", "**/dist/*", "**/build/*", "**/git_modules/*", "**/.svelte-kit/*", "**/public/*", "**/android/*", "**@types/three/*.d.ts"
]
}

View file

@ -17,6 +17,7 @@
},
"dependencies": {
"@typescript-eslint/parser": "^6.12.0",
"comlink": "^4.4.1",
"concurrently": "^8.2.2",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",

View file

@ -11,6 +11,9 @@ importers:
'@typescript-eslint/parser':
specifier: ^6.12.0
version: 6.12.0(eslint@8.54.0)(typescript@5.3.2)
comlink:
specifier: ^4.4.1
version: 4.4.1
concurrently:
specifier: ^8.2.2
version: 8.2.2
@ -1288,7 +1291,7 @@ packages:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies:
'@types/connect': 3.4.38
'@types/node': 20.9.5
'@types/node': 20.10.1
/@types/bonjour@3.5.13:
resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==}
@ -1313,7 +1316,7 @@ packages:
/@types/connect@3.4.38:
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
dependencies:
'@types/node': 20.9.5
'@types/node': 20.10.1
/@types/cookie@0.5.4:
resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==}
@ -1343,7 +1346,7 @@ packages:
/@types/express-serve-static-core@4.17.41:
resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==}
dependencies:
'@types/node': 20.9.5
'@types/node': 20.10.1
'@types/qs': 6.9.10
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@ -1381,7 +1384,7 @@ packages:
/@types/http-proxy@1.17.14:
resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==}
dependencies:
'@types/node': 20.9.5
'@types/node': 20.10.1
/@types/json-schema@7.0.15:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@ -1413,7 +1416,7 @@ packages:
/@types/node-forge@1.3.10:
resolution: {integrity: sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==}
dependencies:
'@types/node': 20.9.5
'@types/node': 20.10.1
/@types/node@18.19.2:
resolution: {integrity: sha512-6wzfBdbWpe8QykUkXBjtmO3zITA0A3FIjoy+in0Y2K4KrCiRhNYJIdwAPDffZ3G6GnaKaSLSEa9ZuORLfEoiwg==}
@ -1467,7 +1470,7 @@ packages:
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
dependencies:
'@types/mime': 1.3.5
'@types/node': 20.9.5
'@types/node': 20.10.1
/@types/serve-index@1.9.4:
resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==}
@ -2796,6 +2799,10 @@ packages:
delayed-stream: 1.0.0
dev: true
/comlink@4.4.1:
resolution: {integrity: sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==}
dev: false
/commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
@ -5148,6 +5155,7 @@ packages:
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
requiresBuild: true
dev: true
/ignore-by-default@1.0.1:

View file

@ -18,10 +18,14 @@
"target": "ESNext",
"moduleResolution": "node",
"module": "ESNext",
"types": ["./types.d.ts"],
"types": [],
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules", "dist", "build", "git_modules", "**/node_modules", "**/dist", "**/build", "**/git_modules",]
"exclude": [
"node_modules", "dist", "build", "git_modules", ".svelte-kit", "public", "android", "@types/three",
"**/node_modules", "**/dist", "**/build", "**/git_modules", "**/.svelte-kit", "**/public", "**/android", "**/@types/three",
"**/node_modules/*", "**/dist/*", "**/build/*", "**/git_modules/*", "**/.svelte-kit/*", "**/public/*", "**/android/*", "**@types/three/*.d.ts"
]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes

View file

@ -1,29 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"baseUrl": "./",
"paths": {
"@/*": ["../common/*"],
"$lib":["./src/lib"],
"$lib/*":["./src/lib/*"]
},
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ESNext",
"moduleResolution": "node",
"module": "ESNext",
"types": ["./types.d.ts"],
"allowSyntheticDefaultImports": true
},
"exclude": ["node_modules", "dist", "build", "git_modules", "**/node_modules", "**/dist", "**/build", "**/git_modules",]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}