diff --git a/.eslintrc.js b/.eslintrc.js index 697ad5a40..a7bd045e8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,6 +3,7 @@ const configExtends = [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:import/errors', 'plugin:import/warnings', ] @@ -55,7 +56,7 @@ module.exports = { { files: ['*.ts', '*.tsx'], parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', 'import', 'react', 'prettier', 'react-hooks'], extends: [ ...configExtends, 'plugin:import/typescript', diff --git a/lib/debug.ts b/lib/debug.ts index 42864dc13..6b56bddad 100644 --- a/lib/debug.ts +++ b/lib/debug.ts @@ -74,22 +74,22 @@ const isRenderer = (process || {}).type === 'renderer' // the very base class abstract class BaseDebugger { - public debug: Console['debug'] = (...args) => this.getLeveledLog('debug')(...args) + public debug: Console['debug'] = (...args: never[]) => this.getLeveledLog('debug')(...args) - public log: Console['log'] = (...args) => this.getLeveledLog('log')(...args) + public log: Console['log'] = (...args: never[]) => this.getLeveledLog('log')(...args) - public info: Console['info'] = (...args) => this.getLeveledLog('info')(...args) + public info: Console['info'] = (...args: never[]) => this.getLeveledLog('info')(...args) - public warn: Console['warn'] = (...args) => this.getLeveledLog('warn')(...args) + public warn: Console['warn'] = (...args: never[]) => this.getLeveledLog('warn')(...args) - public error: Console['error'] = (...args) => this.getLeveledLog('error')(...args) + public error: Console['error'] = (...args: never[]) => this.getLeveledLog('error')(...args) - public trace: Console['trace'] | void = (...args) => this.getLeveledLog('trace')(...args) + public trace: Console['trace'] | void = (...args: never[]) => this.getLeveledLog('trace')(...args) - public table: Console['table'] = (...args) => + public table: Console['table'] = (...args: never[]) => (this.getLeveledLog('table') as Console['table'])(...args) - public assert: Console['assert'] = (...args) => this.getLeveledLog('assert')(...args) + public assert: Console['assert'] = (...args: never[]) => this.getLeveledLog('assert')(...args) protected prefix = '[MAIN]' @@ -114,7 +114,7 @@ abstract class BaseDebugger { protected abstract getLogFunc(level: LogType): DefaultLogger - protected getLeveledLog(level: LogType): (...args: any[]) => void { + protected getLeveledLog(level: LogType): (...args: never[]) => void { if (this.isEnabled()) { switch (level) { case 'assert': @@ -174,7 +174,7 @@ abstract class DebuggerBase extends BaseDebugger { if (!this.extra(tag)) { return Debug.wrap('Invalid extra option name') } - this.extra(tag).enable() + this.extra(tag)?.enable() return Debug.wrap({ enabledExtra: tag }) } @@ -182,7 +182,7 @@ abstract class DebuggerBase extends BaseDebugger { if (!this.validateTagName(tag)) { return Debug.wrap('Invalid extra option name') } - this.extra(tag).disable() + this.extra(tag)?.disable() return Debug.wrap({ disabledExtra: tag }) } @@ -206,14 +206,14 @@ abstract class DebuggerBase extends BaseDebugger { } this.h.set(tag, extraHandler) } - return this.h.get(tag)! + return this.h.get(tag) } public main() { if (!this.h || !this.h.get('main')) { this.h.set('main', new ExtraDebugger('[MAIN]')) } - return this.h.get('main')! + return this.h.get('main') } public init() { diff --git a/lib/ipc.ts b/lib/ipc.ts index 330770c69..49eead7fe 100644 --- a/lib/ipc.ts +++ b/lib/ipc.ts @@ -5,7 +5,7 @@ import { EventEmitter } from 'events' import { mapValues } from 'lodash' class IPC extends EventEmitter { - data: any = new Object() + data: Record never | never>> = {} // scope: string // opts: key-func Object @@ -15,7 +15,7 @@ class IPC extends EventEmitter { return } if (!this.data[scope]) { - this.data[scope] = new Object() + this.data[scope] = {} } this.unregister(scope, Object.keys(opts)) for (const key in opts) { @@ -61,10 +61,10 @@ class IPC extends EventEmitter { // key: string // args: arguments passing to api - foreachCall = (key: string, ...args: any[]) => { + foreachCall = (key: string, ...args: never[]) => { for (const scope in this.data) { if (Object.prototype.hasOwnProperty.call(this.data[scope], key)) { - this.data[key].apply(null, args) + this.data[scope][key].apply(null as never, args) } } } diff --git a/lib/proxy.ts b/lib/proxy.ts index 1e7bcc1ba..ac093321b 100644 --- a/lib/proxy.ts +++ b/lib/proxy.ts @@ -4,8 +4,8 @@ import http from 'http' import path from 'path' import querystring from 'querystring' import mime from 'mime' -import createPacProxyAgent from 'pac-proxy-agent' -import createHttpProxyAgent from 'http-proxy-agent' +import createPacProxyAgent, { PacProxyAgent } from 'pac-proxy-agent' +import createHttpProxyAgent, { HttpProxyAgent } from 'http-proxy-agent' import { SocksProxyAgent } from 'socks-proxy-agent' import { app, session } from 'electron' import util from 'util' @@ -26,7 +26,7 @@ type PoiRequestOptions = http.RequestOptions interface PoiResponseData { statusCode?: number error?: Error - data?: any + data?: unknown } interface KancolleServer { @@ -42,7 +42,7 @@ interface KancolleServerInfo { const gunzipAsync = util.promisify(gunzip) const inflateAsync = util.promisify(inflate) -const delay = (time) => new Promise((res) => setTimeout(res, time)) +const delay = (time: number) => new Promise((res) => setTimeout(res, time)) const isStaticResource = (pathname: string, hostname: string): boolean => { if (pathname.startsWith('/kcs2/')) { @@ -91,13 +91,16 @@ const findHack = (pathname: string): string | null => { const sp = loc.split('.') const ext = sp.pop() sp.push('hack') - sp.push(ext) + if (ext) { + sp.push(ext) + } loc = sp.join('.') try { fs.accessSync(loc, fs.constants.R_OK) return loc } catch (e) { - if (e.code !== 'ENOENT') console.error(`error while loading hack file ${loc}`, e) + if ((e as { code: string })?.code !== 'ENOENT') + console.error(`error while loading hack file ${loc}`, e) return null } } @@ -150,16 +153,16 @@ const resolveProxyUrl = (): string => { } class Proxy extends EventEmitter { - pacAgents = {} - socksAgents = {} - httpAgents = {} + pacAgents: Record = {} + socksAgents: Record = {} + httpAgents: Record = {} serverInfo: KancolleServer = {} serverList: KancolleServerInfo = fs.readJsonSync(path.join(ROOT, 'assets', 'data', 'server.json')) getServerInfo = () => this.serverInfo - server: http.Server - port: number + server!: http.Server + port!: number load = () => { // Handles http request only, https request will be passed to upstream proxy directly. @@ -193,7 +196,12 @@ class Proxy extends EventEmitter { } updateServerInfo = (urlPattern: url.UrlWithStringQuery) => { - if (isKancolleGameApi(urlPattern.pathname) && this.serverInfo.ip !== urlPattern.hostname) { + if ( + urlPattern.pathname && + urlPattern.hostname && + isKancolleGameApi(urlPattern.pathname) && + this.serverInfo.ip !== urlPattern.hostname + ) { if (this.serverList[urlPattern.hostname]) { this.serverInfo = { ...this.serverList[urlPattern.hostname], @@ -210,7 +218,7 @@ class Proxy extends EventEmitter { } createServer = async (req: http.IncomingMessage, res: http.ServerResponse) => { - const urlPattern = url.parse(req.url) + const urlPattern = url.parse(req.url || '') // Prepare request headers delete req.headers['proxy-connection'] @@ -221,7 +229,9 @@ class Proxy extends EventEmitter { // Find cachefile for static resource const cacheFile = - urlPattern.hostname && isStaticResource(urlPattern.pathname, urlPattern.hostname) + urlPattern.hostname && + urlPattern.pathname && + isStaticResource(urlPattern.pathname, urlPattern.hostname) ? findHack(urlPattern.pathname) || findCache(urlPattern.pathname, urlPattern.hostname) : false @@ -246,7 +256,7 @@ class Proxy extends EventEmitter { const reqOption = this.getRequestOptions(urlPattern, req) const { statusCode, data, error } = await this.fetchResponse(reqOption, rawReqBody, res) if (error) { - if (count >= retries || !isKancolleGameApi(urlPattern.pathname)) { + if (count >= retries || !isKancolleGameApi(urlPattern.pathname || '')) { res.end() throw error } @@ -257,7 +267,7 @@ class Proxy extends EventEmitter { res.end() if (statusCode === 200 && data != null) { this.emit('network.on.response', req.method, requestInfo, data, reqBody, Date.now()) - } else if (statusCode >= 400) { + } else if (statusCode == null || statusCode >= 400) { this.emit('network.error', requestInfo, statusCode) } break @@ -265,14 +275,14 @@ class Proxy extends EventEmitter { } } } catch (e) { - error(`${req.method} ${req.url} ${e.toString()}`) + error(`${req.method} ${req.url} ${(e as Error).toString()}`) this.emit('network.error', requestInfo) } } - fetchRequest = (req: http.IncomingMessage): any => + fetchRequest = (req: http.IncomingMessage): Promise => new Promise((resolve) => { - const reqBody = [] + const reqBody: Uint8Array[] = [] req.on('data', (chunk) => { reqBody.push(chunk) }) @@ -296,8 +306,8 @@ class Proxy extends EventEmitter { switch (config.get('proxy.use')) { // HTTP Request via SOCKS5 proxy case 'socks5': { - const socksHost = config.get('proxy.socks5.host', '127.0.0.1') - const socksPort = config.get('proxy.socks5.port', 1080) + const socksHost: string = config.get('proxy.socks5.host', '127.0.0.1') + const socksPort: number = config.get('proxy.socks5.port', 1080) const uri = `${socksHost}:${socksPort}` if (!this.socksAgents[uri]) { this.socksAgents[uri] = new SocksProxyAgent(`socks://${uri}`) @@ -335,48 +345,48 @@ class Proxy extends EventEmitter { } parseResponse = async ( - resDataChunks: any[], + resDataChunks: unknown[], header: http.IncomingHttpHeaders, - ): Promise => { + ): Promise => { const contentType: string = header['content-type'] || (header['Content-Type'] as string) || '' if (!contentType.startsWith('text') && !contentType.startsWith('application')) { - return null + return undefined } - const resData = Buffer.concat(resDataChunks) + const resData = Buffer.concat(resDataChunks as never[]) const contentEncoding = header['content-encoding'] || (header['Content-Encoding'] as string) const isGzip = /gzip/i.test(contentEncoding) const isDeflat = /deflate/i.test(contentEncoding) const unzipped = isGzip ? await gunzipAsync(resData).catch(() => { - return null + return undefined }) : isDeflat ? await inflateAsync(resData).catch(() => { - return null + return undefined }) : resData try { - const str = unzipped.toString() - const parsed = str.startsWith('svdata=') ? str.substring(7) : str - JSON.parse(parsed) + const str = unzipped?.toString() + const parsed = str?.startsWith('svdata=') ? str.substring(7) : str + JSON.parse(parsed || '') return parsed } catch (e) { - return null + return undefined } } fetchResponse = ( options: PoiRequestOptions, - rawReqBody: any, + rawReqBody: unknown, cRes: http.ServerResponse, ): Promise => new Promise((resolve) => { const proxyRequest = http.request(options, (res) => { const { statusCode, headers } = res - const resDataChunks: any[] = [] + const resDataChunks: unknown[] = [] - cRes.writeHead(statusCode, headers) + cRes.writeHead(statusCode || 0, headers) res.pipe(cRes) res.on('data', (chunk) => { @@ -425,7 +435,7 @@ class Proxy extends EventEmitter { res.writeHead(200, { Server: 'nginx', 'Content-Length': data.length, - 'Content-Type': mime.getType(cacheFile), + 'Content-Type': mime.getType(cacheFile) || '', 'Last-Modified': mtime, 'Cache-Control': 'max-age=0', }) @@ -437,15 +447,15 @@ class Proxy extends EventEmitter { delete req.headers['proxy-connection'] req.headers['connection'] = 'close' const remoteUrl = url.parse(`https://${req.url}`) - let remote = null + let remote: net.Socket switch (config.get('proxy.use')) { case 'socks5': { // Write data directly to SOCKS5 proxy remote = socks.createConnection({ socksHost: config.get('proxy.socks5.host', '127.0.0.1'), socksPort: config.get('proxy.socks5.port', 1080), - host: remoteUrl.hostname, - port: remoteUrl.port, + host: remoteUrl.hostname || '', + port: remoteUrl.port || 0, }) remote.on('connect', () => { client.write('HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n') @@ -479,7 +489,7 @@ class Proxy extends EventEmitter { } default: { // Connect to remote directly - remote = net.connect(Number(remoteUrl.port), remoteUrl.hostname, () => { + remote = net.connect(Number(remoteUrl.port), remoteUrl.hostname || undefined, () => { client.write('HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n') remote.write(head) client.pipe(remote) diff --git a/shims/global.d.ts b/shims/global.d.ts index d881b943d..95c2627c6 100644 --- a/shims/global.d.ts +++ b/shims/global.d.ts @@ -3,10 +3,6 @@ interface ToastConfig { title: string } -interface Window { - toast: (message: string, config: ToastConfig) => void -} - declare global { namespace NodeJS { interface Global { @@ -15,6 +11,15 @@ declare global { DEFAULT_CACHE_PATH: string } } + interface Window { + toast: (message: string, config: ToastConfig) => void + } + // let and const do not show up on globalThis + /* eslint-disable no-var */ + var EXROOT: string + var ROOT: string + var DEFAULT_CACHE_PATH: string + /* eslint-enable no-var */ } export {} diff --git a/tsconfig.json b/tsconfig.json index b19b06fd1..48b69be1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,22 @@ "compilerOptions": { "target": "ESNext", "module": "commonjs", - "lib": ["ESNext", "DOM"], + "lib": [ + "ESNext", + "DOM" + ], "jsx": "react", "strict": true, "baseUrl": "./", "paths": { - "views/*": ["./views/*"] + "views/*": [ + "./views/*" + ] }, - "typeRoots": ["./node_modules/@types", "./shims"], + "typeRoots": [ + "./node_modules/@types", + "./shims/vendor" + ], "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, diff --git a/views/components/etc/webview-constants.es b/views/components/etc/webview-constants.es deleted file mode 100644 index a7a46a0db..000000000 --- a/views/components/etc/webview-constants.es +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from 'prop-types' - -export const events = [ - 'load-commit', - 'did-attach', - 'did-finish-load', - 'did-fail-load', - 'did-frame-finish-load', - 'did-start-loading', - 'did-stop-loading', - 'did-get-response-details', - 'did-get-redirect-request', - 'dom-ready', - 'page-title-set', // deprecated event - 'page-title-updated', - 'page-favicon-updated', - 'enter-html-full-screen', - 'leave-html-full-screen', - 'console-message', - 'found-in-page', - 'new-window', - 'will-navigate', - 'did-navigate', - 'did-navigate-in-page', - 'close', - 'ipc-message', - 'crashed', - 'gpu-crashed', - 'plugin-crashed', - 'destroyed', - 'media-started-playing', - 'media-paused', - 'did-change-theme-color', - 'update-target-url', - 'devtools-opened', - 'devtools-closed', - 'devtools-focused', -] - -export const methods = [ - 'loadURL', - 'getURL', - 'getTitle', - 'isLoading', - 'isWaitingForResponse', - 'stop', - 'reload', - 'reloadIgnoringCache', - 'canGoBack', - 'canGoForward', - 'canGoToOffset', - 'clearHistory', - 'goBack', - 'goForward', - 'goToIndex', - 'goToOffset', - 'isCrashed', - 'setUserAgent', - 'getUserAgent', - 'insertCSS', - 'executeJavaScript', - 'openDevTools', - 'closeDevTools', - 'isDevToolsOpened', - 'isDevToolsFocused', - 'inspectElement', - 'inspectServiceWorker', - 'undo', - 'redo', - 'cut', - 'copy', - 'paste', - 'pasteAndMatchStyle', - 'delete', - 'selectAll', - 'unselect', - 'replace', - 'replaceMisspelling', - 'insertText', - 'findInPage', - 'stopFindInPage', - 'print', - 'printToPDF', - 'capturePage', - 'send', - 'sendInputEvent', - 'setZoomFactor', - 'setZoomLevel', - 'showDefinitionForSelection', - 'focus', - 'addEventListener', - 'removeEventListener', -] - -export const props = { - src: PropTypes.string, - autosize: PropTypes.string, - nodeintegration: PropTypes.string, - plugins: PropTypes.bool, - preload: PropTypes.string, - httpreferrer: PropTypes.string, - disablewebsecurity: PropTypes.string, - partition: PropTypes.string, - allowpopups: PropTypes.string, - webpreferences: PropTypes.string, - blinkfeatures: PropTypes.string, - disableblinkfeatures: PropTypes.string, - guestinstance: PropTypes.number, -} - -export const staticProps = { - audioMuted: PropTypes.bool, - userAgent: PropTypes.string, - zoomLevel: PropTypes.number, - zoomFactor: PropTypes.number, - frameRate: PropTypes.number, -} diff --git a/views/components/etc/webview-util.ts b/views/components/etc/webview-util.ts new file mode 100644 index 000000000..1bd41236e --- /dev/null +++ b/views/components/etc/webview-util.ts @@ -0,0 +1,67 @@ +import type { WebviewTag } from 'electron' +import { camelCase } from 'lodash' +import { useEffect } from 'react' +import type { CamelCase } from 'yargs' + +export const webviewEvents = [ + 'load-commit', + 'did-attach', + 'did-finish-load', + 'did-fail-load', + 'did-frame-finish-load', + 'did-start-loading', + 'did-stop-loading', + 'dom-ready', + 'console-message', + 'context-menu', + 'devtools-open-url', + 'devtools-opened', + 'devtools-closed', + 'devtools-focused', + 'will-navigate', + 'did-start-navigation', + 'did-redirect-navigation', + 'did-navigate', + 'did-frame-navigate', + 'did-navigate-in-page', + 'close', + 'render-process-gone', + 'plugin-crashed', + 'destroyed', + 'page-title-updated', + 'page-favicon-updated', + 'enter-html-full-screen', + 'leave-html-full-screen', + 'media-started-playing', + 'media-paused', + 'found-in-page', + 'did-change-theme-color', + 'update-target-url', +] as const + +type EventName = typeof webviewEvents[number] +type HandlerName = CamelCase<`on-${T}`> + +export type HandlerFields = { + [key in HandlerName]?: () => void +} + +export const useWebviewEventListener = ( + eventName: EventName, + handlers: HandlerFields, + view?: WebviewTag, +) => { + useEffect(() => { + const handlerName = camelCase(`on-${eventName}`) as HandlerName + const handler = handlers[handlerName] + if (view && handler) { + view.addEventListener(eventName, handler) + } + + return () => { + if (view && handler) { + view.removeEventListener(eventName, handler) + } + } + }, [eventName, handlers, view]) +} diff --git a/views/components/etc/webview.tsx b/views/components/etc/webview.tsx index 12f2a6c46..5a91b52ad 100644 --- a/views/components/etc/webview.tsx +++ b/views/components/etc/webview.tsx @@ -1,6 +1,5 @@ import React, { CSSProperties, - HTMLProps, useEffect, useMemo, useState, @@ -9,18 +8,26 @@ import React, { } from 'react' import { type DidFailLoadEvent, type WebviewTag, type WebContents } from 'electron' import { webContents } from '@electron/remote' - -interface Props extends HTMLProps { +import { HandlerFields, useWebviewEventListener } from './webview-util' + +type WebviewTagDOMAttrs = Partial< + Pick< + WebviewTag, + 'src' | 'disablewebsecurity' | 'allowpopups' | 'preload' | 'useragent' | 'webpreferences' + > +> + +interface ExtraFields { + className?: string + webviewTagClassName?: string style?: CSSProperties zoomFactor?: number audioMuted?: boolean onResize?: (entries: ResizeObserverEntry[]) => void - onDidAttach?: () => void - onDestroyed?: () => void - onDidFrameFinishLoad?: () => void - onMediaStartedPlaying?: () => void } +type Props = WebviewTagDOMAttrs & HandlerFields & ExtraFields + type ExtendedWebviewTag = WebviewTag & { getWebContents: () => WebContents isReady: () => boolean @@ -33,10 +40,14 @@ const ElectronWebView = forwardRef( zoomFactor, audioMuted, onResize, - onDidAttach, - onDestroyed, - onMediaStartedPlaying, - onDidFrameFinishLoad, + className, + webviewTagClassName, + src, + webpreferences, + disablewebsecurity, + allowpopups, + preload, + useragent, ...props }, ref, @@ -45,12 +56,7 @@ const ElectronWebView = forwardRef( const [isReady, setIsReady] = useState(false) const [entries, setEntries] = useState([]) - useEffect(() => { - if (onResize) { - onResize(entries) - } - }, [onResize, entries]) - + // Sync zoomFactor state useEffect(() => { if ( isReady && @@ -62,6 +68,7 @@ const ElectronWebView = forwardRef( } }, [isReady, view, view?.getZoomFactor, zoomFactor]) + // Sync audioMuted state useEffect(() => { if ( isReady && @@ -75,6 +82,7 @@ const ElectronWebView = forwardRef( const observer = useMemo(() => new ResizeObserver(setEntries), []) + // Enable Observer useEffect(() => { if (view) { observer.observe(view) @@ -87,11 +95,20 @@ const ElectronWebView = forwardRef( } }, [view, observer]) + // onResize event handler + useEffect(() => { + if (onResize) { + onResize(entries) + } + }, [onResize, entries]) + + // Error handling useEffect(() => { const callback = (e: DidFailLoadEvent) => { if (e.errorCode !== -3) { const errorScript = `document.write('
Webview load error
Error Code: ${e.errorCode}
Description: ${e.errorDescription}
URL: ${e.validatedURL}')\ndocument.body.style.backgroundColor = "white"` - ;(e.target as WebviewTag).executeJavaScript(errorScript) + const target = e.target as WebviewTag + target.executeJavaScript(errorScript) } } if (view) { @@ -104,6 +121,7 @@ const ElectronWebView = forwardRef( } }) + // Set isReady state useEffect(() => { const cb = () => { setIsReady(true) @@ -118,54 +136,42 @@ const ElectronWebView = forwardRef( } }, [view]) - useEffect(() => { - if (view && onDidAttach) { - view.addEventListener('did-attach', onDidAttach) - } - - return () => { - if (view && onDidAttach) { - view.removeEventListener('did-attach', onDidAttach) - } - } - }, [onDidAttach, view]) - - useEffect(() => { - if (view && onDidFrameFinishLoad) { - view.addEventListener('did-frame-finish-load', onDidFrameFinishLoad) - } - - return () => { - if (view && onDidFrameFinishLoad) { - view.removeEventListener('did-frame-finish-load', onDidFrameFinishLoad) - } - } - }, [onDidFrameFinishLoad, view]) - - useEffect(() => { - if (view && onMediaStartedPlaying) { - view.addEventListener('media-started-playing', onMediaStartedPlaying) - } - - return () => { - if (view && onMediaStartedPlaying) { - view.removeEventListener('media-started-playing', onMediaStartedPlaying) - } - } - }, [onMediaStartedPlaying, view]) - - useEffect(() => { - if (view && onDestroyed) { - view.addEventListener('destroyed', onDestroyed) - } - - return () => { - if (view && onDestroyed) { - view.removeEventListener('destroyed', onDestroyed) - } - } - }, [onDestroyed, view]) - + // Custom event handlers + useWebviewEventListener('load-commit', props, view) + useWebviewEventListener('did-attach', props, view) + useWebviewEventListener('did-finish-load', props, view) + useWebviewEventListener('did-fail-load', props, view) + useWebviewEventListener('did-frame-finish-load', props, view) + useWebviewEventListener('did-start-loading', props, view) + useWebviewEventListener('did-stop-loading', props, view) + useWebviewEventListener('dom-ready', props, view) + useWebviewEventListener('console-message', props, view) + useWebviewEventListener('context-menu', props, view) + useWebviewEventListener('devtools-open-url', props, view) + useWebviewEventListener('devtools-opened', props, view) + useWebviewEventListener('devtools-closed', props, view) + useWebviewEventListener('devtools-focused', props, view) + useWebviewEventListener('will-navigate', props, view) + useWebviewEventListener('did-start-navigation', props, view) + useWebviewEventListener('did-redirect-navigation', props, view) + useWebviewEventListener('did-navigate', props, view) + useWebviewEventListener('did-frame-navigate', props, view) + useWebviewEventListener('did-navigate-in-page', props, view) + useWebviewEventListener('close', props, view) + useWebviewEventListener('render-process-gone', props, view) + useWebviewEventListener('plugin-crashed', props, view) + useWebviewEventListener('destroyed', props, view) + useWebviewEventListener('page-title-updated', props, view) + useWebviewEventListener('page-favicon-updated', props, view) + useWebviewEventListener('enter-html-full-screen', props, view) + useWebviewEventListener('leave-html-full-screen', props, view) + useWebviewEventListener('media-started-playing', props, view) + useWebviewEventListener('media-paused', props, view) + useWebviewEventListener('found-in-page', props, view) + useWebviewEventListener('did-change-theme-color', props, view) + useWebviewEventListener('update-target-url', props, view) + + // Custom ref useImperativeHandle( ref, () => { @@ -191,9 +197,15 @@ const ElectronWebView = forwardRef( ) return ( -
+
{ setView(view) }} diff --git a/views/components/settings/about/index.es b/views/components/settings/about/index.es index 97c5c265a..bf0e816ee 100644 --- a/views/components/settings/about/index.es +++ b/views/components/settings/about/index.es @@ -35,7 +35,7 @@ export const About = () => { observer.current.observe(sentinel.current) return () => observer.current.disconnect() - }, []) + }, [handleIntersection]) return (
diff --git a/views/components/settings/network/connection-test.tsx b/views/components/settings/network/connection-test.tsx index a62fc3655..b1ab5297e 100644 --- a/views/components/settings/network/connection-test.tsx +++ b/views/components/settings/network/connection-test.tsx @@ -47,7 +47,7 @@ export const ConnectionTest: FunctionComponent = () => { controller.current?.abort?.() setLoading(false) } - }, []) + }, [connect, t]) return (
diff --git a/views/components/settings/plugin/index.es b/views/components/settings/plugin/index.es index d0c9d29aa..3cc5134b1 100644 --- a/views/components/settings/plugin/index.es +++ b/views/components/settings/plugin/index.es @@ -9,7 +9,6 @@ import { connect } from 'react-redux' import { withNamespaces } from 'react-i18next' import Promise from 'bluebird' import { - Card, Callout, Intent, Button, diff --git a/views/env-parts/theme.es b/views/env-parts/theme.es index 4ec1a5674..8123b3c76 100644 --- a/views/env-parts/theme.es +++ b/views/env-parts/theme.es @@ -242,15 +242,19 @@ export function loadStyle( // Workaround for window transparency on 27.0.0 if (process.platform === 'win32') { - currentWindow.on('blur', () => { + const resetBackgroundColor = () => { if (config.get('poi.appearance.vibrant', 0) === 1) { currentWindow.setBackgroundColor('#00000000') } - }) + } - currentWindow.on('focus', () => { + currentWindow.on('blur', resetBackgroundColor) + currentWindow.on('focus', resetBackgroundColor) + currentWindow.on('restore', () => { if (config.get('poi.appearance.vibrant', 0) === 1) { - currentWindow.setBackgroundColor('#00000000') + const [width, height] = currentWindow.getSize() + currentWindow.setSize(width + 1, height + 1) + currentWindow.setSize(width, height) } }) } diff --git a/views/kan-game-wrapper.es b/views/kan-game-wrapper.es index 26b6ae23c..404dbf7a6 100644 --- a/views/kan-game-wrapper.es +++ b/views/kan-game-wrapper.es @@ -39,17 +39,23 @@ const KanGame = styled(CustomTag)` overflow: hidden; width: 100%; - .kancolle-webview { + .bp4-toast-container { + overflow: hidden !important; + } +` + +const KanGameWebview = styled(WebView)` + width: 100%; + padding-top: 60%; + position: relative; + + webview { height: 100%; left: 0; position: absolute; top: 0; width: 100%; } - - .bp4-toast-container { - overflow: hidden !important; - } ` @connect((state) => ({ @@ -281,22 +287,17 @@ export class KanGameWrapper extends Component { .replace(/poi[^ ]* /, '') .replace(bypassGoogleRestriction ? /Chrome[^ ]* / : '', '') const webview = ( - { - const { - api_area_id, - api_base_id_src, - api_base_id, - api_item_id, - } = payload.postBody + const { api_area_id, api_base_id_src, api_base_id, api_item_id } = payload.postBody const { api_base_items } = payload.body // Err on the side of caution, few preconditions before updating. @@ -77,7 +72,7 @@ const airBaseSlice = createSlice({ const findSquadronIndex = (baseId: number | string) => { const ret = findIndex( state, - (squad) => squad.api_rid === +baseId && squad.api_area_id === +api_area_id + (squad) => squad.api_rid === +baseId && squad.api_area_id === +api_area_id, ) return ret === -1 ? +baseId - 1 : ret } @@ -90,7 +85,8 @@ const airBaseSlice = createSlice({ if ( findIndex( state[indexSrc].api_plane_info, - (squad) => squad.api_slotid === +api_item_id) === -1 + (squad) => squad.api_slotid === +api_item_id, + ) === -1 ) { return state } @@ -113,10 +109,7 @@ const airBaseSlice = createSlice({ return compareUpdate( state, - constructArray( - [indexSrc, indexDst], - map([objSrc, objDst], convertItem), - ), + constructArray([indexSrc, indexDst], map([objSrc, objDst], convertItem)), 3, ) }) @@ -195,7 +188,7 @@ const airBaseSlice = createSlice({ return airbase } - const index = api_rid! - 1 + const index = (api_rid || 0) - 1 const newBase = { ...airbase } if (get(api_f_maxhps, index) >= 0) { diff --git a/views/utils/__tests__/tools.spec.ts b/views/utils/__tests__/tools.spec.ts index 8123a44d4..b37ab9b41 100644 --- a/views/utils/__tests__/tools.spec.ts +++ b/views/utils/__tests__/tools.spec.ts @@ -2,7 +2,9 @@ import _ from 'lodash' import path from 'path' import { isSubdirectory, compareUpdate, cjkSpacing, constructArray } from '../tools' -const pathPatterns = [ +type Pattern = [string, string, boolean] + +const pathPatterns: Pattern[] = [ ['/foo', '/foo', true], ['/foo', '/bar', false], ['/foo', '/foo/bar', true], @@ -14,7 +16,7 @@ const pathPatterns = [ ['/foo/bar', './bar', false], ] -const win32PathPatterns = [ +const win32PathPatterns: Pattern[] = [ ['C:\\Foo', 'C:\\Foo\\Bar', true], ['C:\\foo', 'D:\\foo', false], ['C:\\foo', 'D:\\foo\\bar', false], @@ -47,7 +49,7 @@ describe('views/utils/tools', () => { }) describe('compareUpdate', () => { - const test = (a: any, b: any, d?: number) => { + const test = (a: unknown, b: unknown, d?: number) => { const c = compareUpdate(a, b, d) return [c !== a, c] } diff --git a/views/utils/tools.ts b/views/utils/tools.ts index 4ab7ff11c..961819f0c 100644 --- a/views/utils/tools.ts +++ b/views/utils/tools.ts @@ -17,6 +17,8 @@ import _, { toString, Dictionary, padStart, + setWith, + clone, } from 'lodash' import pangu from 'pangu' import path from 'path' @@ -115,19 +117,19 @@ export function buildArray( pairsOrIdx: number | [number, T][], _value?: T, ): (T | undefined)[] { - let pairs: [number, T][] + let pairs: [number, T | undefined][] if (Array.isArray(pairsOrIdx)) { pairs = pairsOrIdx } else { console.warn( 'buildArray(idx, value) is pending deprecation, please use buildArray([idx, value]) instead', ) - pairs = [[pairsOrIdx, _value!]] + pairs = [[pairsOrIdx, _value]] } const ret: T[] = [] pairs.forEach(([index, value]) => { index = Math.floor(index) - if (isNaN(index) || index < 0) { + if (isNaN(index) || index < 0 || value == null) { return } ret[index] = value @@ -181,36 +183,12 @@ export function pickExisting(state: T, body: object): T { * @param path data path * @param val the value to update */ -export function reduxSet(obj: T, path: (string | number)[], val: any): T { - const [prop, ...restPath] = path - if (typeof prop === 'undefined') { - if (!isEqual(obj, val)) { - return val - } else { - return obj - } - } - let before - if (prop in obj) { - before = obj[prop as keyof T] - } else { - before = {} - } - const after = reduxSet(before, restPath, val) - if (after !== before) { - let result - if (Array.isArray(obj)) { - result = obj.slice() - result[prop as number] = after - } else { - result = { - ...obj, - [prop]: after, - } - } - return result as T - } - return obj +export function reduxSet>( + obj: T, + path: (string | number)[], + val: any, +): T { + return setWith(clone(obj), path, val, clone) } /**