From 9827958a0993ff1c0dbdf20fd4e943f7bfe567b5 Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Sun, 10 Sep 2023 18:28:00 +0200 Subject: [PATCH] chore: restructure main --- jsconfig.json | 6 +- src/background/background.js | 2 + src/main/discord.js | 94 +++++++++++ src/main/main.js | 308 +++-------------------------------- src/main/protocol.js | 75 +++++++++ src/main/updater.js | 24 +++ src/main/util.js | 93 +++++++++++ src/preload/preload.js | 4 +- 8 files changed, 315 insertions(+), 291 deletions(-) create mode 100644 src/main/discord.js create mode 100644 src/main/protocol.js create mode 100644 src/main/updater.js create mode 100644 src/main/util.js diff --git a/jsconfig.json b/jsconfig.json index 8aa45c8..08f3c87 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,7 +3,11 @@ "baseUrl": "./", "paths": { "@/*": ["src/renderer/*"], - } + }, + "checkJs": true, + "target": "ESNext", + "moduleResolution": "node", + "module": "ESNext" }, "exclude": ["node_modules/**", "**/node_modules", "dist", "build"] } \ No newline at end of file diff --git a/src/background/background.js b/src/background/background.js index 51a397b..2362a98 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -28,6 +28,7 @@ class TorrentClient extends WebTorrent { this.handleMessage({ data }) } }) + ipcRenderer.on('destroy', this.predestroy.bind(this)) }) this.settings = settings @@ -192,4 +193,5 @@ class TorrentClient extends WebTorrent { } } +// @ts-ignore window.client = new TorrentClient() diff --git a/src/main/discord.js b/src/main/discord.js new file mode 100644 index 0000000..c78dee2 --- /dev/null +++ b/src/main/discord.js @@ -0,0 +1,94 @@ +import { Client } from 'discord-rpc' +import { ipcMain } from 'electron' + +export default class { + window + status + discord + requestedDiscordDetails + allowDiscordDetails + rpcStarted + cachedPresence + + /** + * @param {import('electron').BrowserWindow} window + */ + constructor (window) { + this.window = window + this.discord = new Client({ + transport: 'ipc' + }) + ipcMain.on('discord_status', (event, data) => { + this.requestedDiscordDetails = data + if (!this.rpcStarted) { + this.handleRPC() + setInterval(this.handleRPC.bind(this), 5000) // According to Discord documentation, clients can only update their presence 5 times per 20 seconds. We will add an extra second to be safe. + this.rpcStarted = true + } + }) + + ipcMain.on('discord', (event, data) => { + this.cachedPresence = data + if (this.allowDiscordDetails) { + this.setDiscordRPC(data) + } + }) + + this.discord.on('ready', async () => { + this.setDiscordRPC(this.status) + this.discord.subscribe('ACTIVITY_JOIN_REQUEST') + this.discord.subscribe('ACTIVITY_JOIN') + this.discord.subscribe('ACTIVITY_SPECTATE') + }) + this.discord.on('ACTIVITY_JOIN', (args) => { + this.window.webContents.send('w2glink', args.secret) + }) + + this.loginRPC() + } + + loginRPC () { + this.discord.login({ clientId: '954855428355915797' }).catch(() => { + setTimeout(this.loginRPC.bind(this), 5000).unref() + }) + } + + setDiscordRPC (data = { + activity: { + timestamps: { + start: Date.now() + }, + details: 'Stream anime torrents, real-time.', + state: 'Watching anime', + assets: { + small_image: 'logo', + small_text: 'https://github.com/ThaUnknown/miru' + }, + buttons: [ + { + label: 'Download app', + url: 'https://github.com/ThaUnknown/miru/releases/latest' + } + ], + instance: true, + type: 3 + } + }) { + this.status = data + if (this.discord.user && this.status) { + this.status.pid = process.pid + this.discord.request('SET_ACTIVITY', this.status) + } + } + + handleRPC () { + if (this.allowDiscordDetails === this.requestedDiscordDetails) return + + this.allowDiscordDetails = this.requestedDiscordDetails + if (!this.allowDiscordDetails) { + this.setDiscordRPC(null) + } else if (this.cachedPresence) { + this.setDiscordRPC(this.cachedPresence) + } + } +} diff --git a/src/main/main.js b/src/main/main.js index 5074475..608f176 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -1,144 +1,23 @@ -import { app, BrowserWindow, protocol, shell, ipcMain, dialog, MessageChannelMain } from 'electron' +/* eslint-disable no-new */ +import { app, BrowserWindow, shell, ipcMain, dialog, MessageChannelMain } from 'electron' import path from 'path' -import { Client } from 'discord-rpc' -import log from 'electron-log' -import { autoUpdater } from 'electron-updater' - -const flags = [ - ['enable-gpu-rasterization'], - ['enable-zero-copy'], - ['ignore-gpu-blocklist'], - ['enable-hardware-overlays', 'single-fullscreen,single-on-top,underlay'], - ['enable-features', 'PlatformEncryptedDolbyVision,EnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilationEnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation'], - ['force_high_performance_gpu'], - ['disable-features', 'Vulkan'], - ['disable-color-correct-rendering'], - ['force-color-profile', 'srgb'] -] -for (const [flag, value] of flags) { - app.commandLine.appendSwitch(flag, value) -} - -if (process.defaultApp) { - if (process.argv.length >= 2) { - app.setAsDefaultProtocolClient('miru', process.execPath, [path.resolve(process.argv[1])]) - } -} else { - app.setAsDefaultProtocolClient('miru') - if (process.argv.length >= 2) { - ipcMain.on('version', () => { - for (const line of process.argv) { - handleProtocol(line) - } - }) - } -} - -if (!app.requestSingleInstanceLock()) { - app.quit() -} else { - app.on('second-instance', (event, commandLine, workingDirectory) => { - // Someone tried to run a second instance, we should focus our window. - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.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 - for (const line of commandLine) { - handleProtocol(line) - } - }) -} -app.on('open-url', (event, url) => { - event.preventDefault() - handleProtocol(url) -}) - -// schema: miru://key/value -const protocolMap = { - auth: sendToken, - anime: id => mainWindow.webContents.send('open-anime', id), - w2g: link => mainWindow.webContents.send('w2glink', link), - schedule: () => mainWindow.webContents.send('schedule'), - donate: () => shell.openExternal('https://github.com/sponsors/ThaUnknown/') -} -const protocolRx = /miru:\/\/([a-z0-9]+)\/(.*)/i -function handleProtocol (text) { - const match = text.match(protocolRx) - if (match) protocolMap[match[1]]?.(match[2]) -} - -function sendToken (line) { - let token = line.split('access_token=')[1].split('&token_type')[0] - if (token) { - if (token.endsWith('/')) token = token.slice(0, -1) - mainWindow.webContents.send('altoken', token) - } -} - -ipcMain.on('open', (event, url) => { - shell.openExternal(url) -}) +import Discord from './discord.js' +import Updater from './updater.js' +import Protocol from './protocol.js' +import { 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 -ipcMain.on('devtools', () => { - webtorrentWindow.webContents.openDevTools() -}) - -ipcMain.on('doh', (event, dns) => { - app.configureHostResolver({ - secureDnsMode: 'secure', - secureDnsServers: [dns] - }) -}) - -app.setJumpList?.([ - { - name: 'Frequent', - items: [ - { - type: 'task', - program: process.execPath, - args: 'miru://schedule/', - title: 'Airing Schedule', - description: 'Open The Airing Schedule' - }, - { - type: 'task', - program: process.execPath, - args: 'miru://w2g/', - title: 'Watch Together', - description: 'Create a New Watch Together Lobby' - }, - { - type: 'task', - program: process.execPath, - args: 'miru://donate/', - title: 'Donate', - description: 'Support This App' - } - ] - } -]) - -ipcMain.on('close', () => { - app.quit() -}) - function createWindow () { - const development = process.env.NODE_ENV?.trim() === 'development' // Create the browser window. webtorrentWindow = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false, - enableRemoteModule: true, backgroundThrottling: false } }) @@ -162,56 +41,33 @@ function createWindow () { icon: path.join(__dirname, '/logo.ico'), show: false }) + new Discord(mainWindow) + new Protocol(mainWindow) + new Updater(mainWindow) mainWindow.setMenuBarVisibility(false) - // console.log(mainWindow.setThumbarButtons([ - // { - // tooltip: 'button1', - // icon: nativeImage.createFromPath('path'), - // click () { console.log('button1 clicked') } - // }, { - // tooltip: 'button2', - // icon: nativeImage.createFromPath('path'), - // click () { console.log('button2 clicked.') } - // } - // ])) - - protocol.registerHttpProtocol('miru', (req, cb) => { - const token = req.url.slice(7) - if (development) { - mainWindow.loadURL(path.join(__dirname, '/app.html' + token)) - } else { - mainWindow.loadURL('http://localhost:5000/app.html' + token) - } - }) - mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['https://sneedex.moe/api/public/nyaa', atob('aHR0cDovL2FuaW1ldG9zaG8ub3JnL3N0b3JhZ2UvdG9ycmVudC8q'), atob('aHR0cHM6Ly9ueWFhLnNpLyo=')] }, ({ responseHeaders }, fn) => { - responseHeaders['Access-Control-Allow-Origin'] = '*' - + responseHeaders['Access-Control-Allow-Origin'] = ['*'] fn({ responseHeaders }) }) - let torrentLoad = null + const torrentLoad = webtorrentWindow.loadURL(development ? 'http://localhost:5000/background.html' : path.join(__dirname, '/background.html')) + mainWindow.loadURL(development ? 'http://localhost:5000/app.html' : path.join(__dirname, '/app.html')) - if (!development) { - // Load production build - torrentLoad = webtorrentWindow.loadFile(path.join(__dirname, '/background.html')) - mainWindow.loadFile(path.join(__dirname, '/app.html')) - } else { - // Load vite dev server page - console.log('Development mode') - torrentLoad = webtorrentWindow.loadURL('http://localhost:5000/background.html') + if (development) { webtorrentWindow.webContents.openDevTools() - mainWindow.loadURL('http://localhost:5000/app.html') mainWindow.webContents.openDevTools() } - // Emitted when the window is closed. mainWindow.on('closed', () => { - // Dereference the window object, usually you would store windows - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. mainWindow = null + webtorrentWindow.webContents.postMessage('destroy', null) + app.quit() + }) + + ipcMain.on('close', () => { + mainWindow = null + webtorrentWindow.webContents.postMessage('destroy', null) app.quit() }) @@ -243,132 +99,8 @@ function createWindow () { }) } -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. app.on('ready', createWindow) app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. if (mainWindow === null) createWindow() }) - -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) - } -}) - -let status = null -const discord = new Client({ - transport: 'ipc' -}) - -function setDiscordRPC (data) { - if (!data) { - data = { - activity: { - timestamps: { - start: Date.now() - }, - details: 'Stream anime torrents, real-time.', - state: 'Watching anime', - assets: { - small_image: 'logo', - small_text: 'https://github.com/ThaUnknown/miru' - }, - buttons: [ - { - label: 'Download app', - url: 'https://github.com/ThaUnknown/miru/releases/latest' - } - ], - instance: true, - type: 3 - } - } - } - status = data - if (discord?.user && status) { - status.pid = process.pid - discord.request('SET_ACTIVITY', status) - } -} - -let allowDiscordDetails = false -let requestedDiscordDetails = false -let rpcStarted = false -let cachedPresence = null - -ipcMain.on('discord_status', (event, data) => { - requestedDiscordDetails = data - if (!rpcStarted) { - handleRPC() - setInterval(handleRPC, 5000) // According to Discord documentation, clients can only update their presence 5 times per 20 seconds. We will add an extra second to be safe. - rpcStarted = true - } -}) - -function handleRPC () { - if (allowDiscordDetails === requestedDiscordDetails) return - - allowDiscordDetails = requestedDiscordDetails - if (!allowDiscordDetails) { - setDiscordRPC(null) - } else if (cachedPresence) { - setDiscordRPC(cachedPresence) - } -} - -ipcMain.on('discord', (event, data) => { - cachedPresence = data - if (allowDiscordDetails) { - setDiscordRPC(data) - } -}) -discord.on('ready', async () => { - setDiscordRPC(status) - discord.subscribe('ACTIVITY_JOIN_REQUEST') - discord.subscribe('ACTIVITY_JOIN') - discord.subscribe('ACTIVITY_SPECTATE') -}) -discord.on('ACTIVITY_JOIN', (args) => { - BrowserWindow.getAllWindows()[0]?.send('w2glink', args.secret) -}) - -function loginRPC () { - discord.login({ clientId: '954855428355915797' }).catch(() => { - setTimeout(loginRPC, 5000).unref() - }) -} -loginRPC() - -ipcMain.on('version', (event) => { - event.sender.send('version', app.getVersion()) // fucking stupid -}) - -autoUpdater.logger = log -autoUpdater.logger.transports.file.level = 'info' -ipcMain.on('update', () => { - autoUpdater.checkForUpdatesAndNotify() -}) - -autoUpdater.checkForUpdatesAndNotify() -autoUpdater.on('update-available', () => { - BrowserWindow.getAllWindows()[0]?.send('update-available', true) -}) -autoUpdater.on('update-downloaded', () => { - BrowserWindow.getAllWindows()[0]?.send('update-downloaded', true) -}) diff --git a/src/main/protocol.js b/src/main/protocol.js new file mode 100644 index 0000000..ef931d8 --- /dev/null +++ b/src/main/protocol.js @@ -0,0 +1,75 @@ +import { app, protocol, shell, ipcMain } from 'electron' +import { development } from './util.js' +import path from 'path' + +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient('miru', process.execPath, [path.resolve(process.argv[1])]) + } +} else { + app.setAsDefaultProtocolClient('miru') +} + +export default class { + // 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'), + donate: () => shell.openExternal('https://github.com/sponsors/ThaUnknown/') + } + + protocolRx = /miru:\/\/([a-z0-9]+)\/(.*)/i + + /** + * @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 : path.join(__dirname, '/app.html' + token)) + }) + + app.on('open-url', (event, url) => { + event.preventDefault() + this.handleProtocol(url) + }) + + if (process.argv.length >= 2 && !process.defaultApp) { + ipcMain.on('version', () => { + for (const line of process.argv) { + this.handleProtocol(line) + } + }) + } + + 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() + } + // 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 + for (const line of commandLine) { + this.handleProtocol(line) + } + }) + } + + sendToken (line) { + 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) + } + } + + handleProtocol (text) { + const match = text.match(this.protocolRx) + if (match) this.protocolMap[match[1]]?.(match[2]) + } +} diff --git a/src/main/updater.js b/src/main/updater.js new file mode 100644 index 0000000..03e440f --- /dev/null +++ b/src/main/updater.js @@ -0,0 +1,24 @@ +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) { + autoUpdater.on('update-available', () => { + window.webContents.send('update-available', true) + }) + autoUpdater.on('update-downloaded', () => { + window.webContents.send('update-downloaded', true) + }) + } +} diff --git a/src/main/util.js b/src/main/util.js new file mode 100644 index 0000000..b0ec586 --- /dev/null +++ b/src/main/util.js @@ -0,0 +1,93 @@ +import { app, ipcMain, shell, dialog } from 'electron' + +export const development = process.env.NODE_ENV?.trim() === 'development' + +const flags = [ + ['enable-gpu-rasterization'], + ['enable-zero-copy'], + ['ignore-gpu-blocklist'], + ['enable-hardware-overlays', 'single-fullscreen,single-on-top,underlay'], + ['enable-features', 'PlatformEncryptedDolbyVision,EnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilationEnableDrDc,CanvasOopRasterization,BackForwardCache:TimeToLiveInBackForwardCacheInSeconds/300/should_ignore_blocklists/true/enable_same_site/true,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation'], + ['force_high_performance_gpu'], + ['disable-features', 'Vulkan'], + ['disable-color-correct-rendering'], + ['force-color-profile', 'srgb'] +] +for (const [flag, value] of flags) { + app.commandLine.appendSwitch(flag, value) +} + +if (!app.requestSingleInstanceLock()) app.quit() + +ipcMain.on('open', (event, url) => { + shell.openExternal(url) +}) + +ipcMain.on('doh', (event, dns) => { + app.configureHostResolver({ + secureDnsMode: 'secure', + secureDnsServers: [dns] + }) +}) + +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', + items: [ + { + type: 'task', + program: 'miru://schedule/', + title: 'Airing Schedule', + description: 'Open The Airing Schedule' + }, + { + type: 'task', + program: 'miru://w2g/', + title: 'Watch Together', + description: 'Create a New Watch Together Lobby' + }, + { + type: 'task', + program: 'miru://donate/', + title: 'Donate', + description: 'Support This App' + } + ] + } +]) +// mainWindow.setThumbarButtons([ +// { +// tooltip: 'button1', +// icon: nativeImage.createFromPath('path'), +// click () { console.log('button1 clicked') } +// }, { +// tooltip: 'button2', +// icon: nativeImage.createFromPath('path'), +// click () { console.log('button2 clicked.') } +// } +// ]) diff --git a/src/preload/preload.js b/src/preload/preload.js index b875c9d..ba05348 100644 --- a/src/preload/preload.js +++ b/src/preload/preload.js @@ -25,8 +25,8 @@ ipcRenderer.once('port', ({ ports }) => { onmessage: (cb) => { ports[0].onmessage = ({ type, data }) => cb({ type, data }) }, - postMessage: (...args) => { - ports[0].postMessage(...args) + postMessage: (a, b) => { + ports[0].postMessage(a, b) } }) })