From f8e5eb2e53fc0eea54518c2dba4ae9b370083eec Mon Sep 17 00:00:00 2001 From: yunlicn Date: Tue, 30 Nov 2021 22:45:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Champion=20Rankings=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.ts | 126 ++++++++++++++++++++ app/utils/config.ts | 7 ++ config/paths.js | 4 +- config/webpack.config.js | 35 ++++++ interfaces/commonTypes.d.ts | 10 ++ public/statistics.html | 55 +++++++++ src/components/toolbar/index.tsx | 16 ++- src/modules/i18n/en-us.ts | 1 + src/modules/i18n/fr-fr.ts | 1 + src/modules/i18n/zh-cn.ts | 1 + src/modules/statistics/content.tsx | 90 +++++++++++++++ src/modules/statistics/index.tsx | 23 ++++ src/modules/statistics/style.module.scss | 139 +++++++++++++++++++++++ src/modules/statistics/toolbar.tsx | 42 +++++++ src/modules/statistics/utils.ts | 77 +++++++++++++ src/statistics.index.tsx | 6 + 16 files changed, 631 insertions(+), 2 deletions(-) create mode 100644 public/statistics.html create mode 100644 src/modules/statistics/content.tsx create mode 100644 src/modules/statistics/index.tsx create mode 100644 src/modules/statistics/style.module.scss create mode 100644 src/modules/statistics/toolbar.tsx create mode 100644 src/modules/statistics/utils.ts create mode 100644 src/statistics.index.tsx diff --git a/app/main.ts b/app/main.ts index 2d53533d..6f6c1c49 100644 --- a/app/main.ts +++ b/app/main.ts @@ -56,6 +56,7 @@ if (ignoreSystemScale) { // Prevent window from being garbage collected let mainWindow: BrowserWindow | null; let popupWindow: BrowserWindow | null; +let statisticsWindow: BrowserWindow | null; let tray = null; let lcuWatcher: LcuWatcher | null = null; @@ -90,6 +91,7 @@ const createMainWindow = async () => { // For multiple windows store them in an array mainWindow = null; popupWindow = null; + statisticsWindow = null; }); await win.loadURL( @@ -143,6 +145,52 @@ const createPopupWindow = async () => { return popup; }; +const createStatisticsWindow = async () => { + const [mX, mY] = mainWindow!.getPosition(); + const curDisplay = screen.getDisplayNearestPoint({ + x: mX, + y: mY, + }); + + const statisticsConfig = appConfig.get(`statistics`); + const statistics = new BrowserWindow({ + show: false, + frame: false, + resizable: true, + fullscreenable: false, + + skipTaskbar: statisticsConfig.alwaysOnTop, + alwaysOnTop: statisticsConfig.alwaysOnTop, + width: statisticsConfig.width || 400, + height: statisticsConfig.height || 650, + x: curDisplay.bounds.width / 2 + 201, + y: curDisplay.workAreaSize.height / 2 - 325, + webPreferences, + }); + + statistics.on( + `move`, + _debounce(() => persistStatisticsBounds(statistics), 1000), + ); + + statistics.on( + `resize`, + _debounce(() => persistStatisticsBounds(statistics), 1000), + ); + + statistics.on('closed', () => { + statisticsWindow = null; + }); + + await statistics.loadURL( + isDev + ? `http://127.0.0.1:3000/statistics.html` + : `file://${path.join(__dirname, 'statistics.html')}`, + ); + + return statistics; +}; + // Prevent multiple instances of the app if (!app.requestSingleInstanceLock()) { app.quit(); @@ -161,6 +209,7 @@ app.on('second-instance', () => { app.on(`quit`, () => { mainWindow = null; popupWindow = null; + statisticsWindow = null; }); app.on('window-all-closed', () => { @@ -187,6 +236,18 @@ function persistPopUpBounds(w: BrowserWindow) { appConfig.set(`popup.height`, height); } +function persistStatisticsBounds(w: BrowserWindow) { + if (!w) { + return; + } + + const { x, y, width, height } = w.getBounds(); + appConfig.set(`statistics.x`, x); + appConfig.set(`statistics.y`, y); + appConfig.set(`statistics.width`, width); + appConfig.set(`statistics.height`, height); +} + let lastChampion = 0; async function onShowPopup(data: IPopupEventData) { @@ -217,11 +278,43 @@ async function onShowPopup(data: IPopupEventData) { }, 300); } +async function onShowStatistics(data: IPopupEventData) { + if (!data.championId || lastChampion === data.championId) { + return; + } + + lastChampion = data.championId; + if (!statisticsWindow) { + statisticsWindow = await createStatisticsWindow(); + } + + // popupWindow.setAlwaysOnTop(true); + statisticsWindow.show(); + // popupWindow.setAlwaysOnTop(false); + // app.focus(); + statisticsWindow.focus(); + + const task = setInterval(() => { + if (!statisticsWindow!.isVisible()) { + return; + } + + statisticsWindow!.webContents.send(`for-statistics`, { + championId: data.championId, + }); + clearInterval(task); + }, 300); +} + function registerMainListeners() { ipcMain.on(`toggle-main-window`, () => { toggleMainWindow(); }); + ipcMain.on(`toggle-statistics-window`, () => { + toggleStatisticsWindow(); + }); + ipcMain.on(`restart-app`, () => { app.relaunch(); app.exit(); @@ -237,6 +330,16 @@ function registerMainListeners() { appConfig.set(`popup.alwaysOnTop`, next); }); + ipcMain.on(`statistics:toggle-always-on-top`, () => { + if (!statisticsWindow) return; + + const next = !statisticsWindow.isAlwaysOnTop(); + statisticsWindow.setAlwaysOnTop(next); + statisticsWindow.setSkipTaskbar(next); + + appConfig.set(`statistics.alwaysOnTop`, next); + }); + ipcMain.on(`popup:reset-position`, () => { const [mx, my] = mainWindow!.getPosition(); const { bounds } = screen.getDisplayNearestPoint({ x: mx, y: my }); @@ -287,6 +390,10 @@ function registerMainListeners() { app.quit(); }); + ipcMain.on(`quit-statistics`, () => { + statisticsWindow?.close(); + }); + ipcMain.on(`applyRunePage`, async (_ev, data: IRuneItem & { jobId: string }) => { try { await lcuWatcher?.applyRunePage(data); @@ -303,6 +410,10 @@ function registerMainListeners() { ipcMain.on(`showPopup`, (_ev, data: IPopupEventData) => { onShowPopup(data); }); + + ipcMain.on(`showStatistics`, (_ev, data: IPopupEventData) => { + onShowStatistics(data); + }); } function toggleMainWindow() { @@ -320,6 +431,21 @@ function toggleMainWindow() { } } +function toggleStatisticsWindow() { + if (!statisticsWindow) { + return; + } + + const visible = statisticsWindow.isVisible(); + if (!visible) { + statisticsWindow.show(); + statisticsWindow.setSkipTaskbar(false); + } else { + statisticsWindow.hide(); + statisticsWindow.setSkipTaskbar(true); + } +} + function makeTray() { const iconPath = path.join( isDev ? `${__dirname}/../` : process.resourcesPath, diff --git a/app/utils/config.ts b/app/utils/config.ts index 9e7d0d0a..465d8de0 100644 --- a/app/utils/config.ts +++ b/app/utils/config.ts @@ -19,6 +19,13 @@ export const appConfig = new Store({ y: null, alwaysOnTop: true, }, + statistics: { + width: 400, + height: 650, + x: null, + y: null, + alwaysOnTop: false, + }, sourceList: DefaultSourceList, lolDirHasCJKChar: false, }, diff --git a/config/paths.js b/config/paths.js index e3576bf7..7713cf89 100644 --- a/config/paths.js +++ b/config/paths.js @@ -1,4 +1,4 @@ -'use strict'; + const path = require('path'); const fs = require('fs'); @@ -82,8 +82,10 @@ module.exports = { appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), popupHtml: resolveApp('public/popup.html'), + statisticsHtml: resolveApp('public/statistics.html'), appIndexJs: resolveModule(resolveApp, 'src/index'), popupIndexJs: resolveModule(resolveApp, 'src/popup.index'), + statisticsIndexJs: resolveModule(resolveApp, 'src/statistics.index'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), diff --git a/config/webpack.config.js b/config/webpack.config.js index 885bad71..d6b4ab21 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -125,6 +125,11 @@ module.exports = function (webpackEnv) { isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'), paths.popupIndexJs, ].filter(Boolean), + + statistics: [ + isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'), + paths.statisticsIndexJs, + ].filter(Boolean), }, target: 'web', @@ -524,6 +529,36 @@ module.exports = function (webpackEnv) { : undefined, ), ), + + new HtmlWebpackPlugin( + Object.assign( + { + chunks: [`statistics`], + filename: `./statistics.html`, + }, + { + inject: true, + template: paths.statisticsHtml, + }, + isEnvProduction + ? { + enableGA: true, + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, + } + : undefined, + ), + ), // Inlines the webpack runtime script. This script is too small to warrant // a network request. // https://github.com/facebook/create-react-app/issues/5358 diff --git a/interfaces/commonTypes.d.ts b/interfaces/commonTypes.d.ts index 655d15d9..222c8d88 100644 --- a/interfaces/commonTypes.d.ts +++ b/interfaces/commonTypes.d.ts @@ -84,6 +84,16 @@ export interface IChampionInfo { id: string; } +export interface IChampionRank { + version: string; + rank: string; + id: string; + position: string; + winRate: string; + pickRate: string; + tier: string | undefined; +} + export interface IFileResult { champion: string; position: string; diff --git a/public/statistics.html b/public/statistics.html new file mode 100644 index 00000000..8f1d04c7 --- /dev/null +++ b/public/statistics.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + ChampR + + +
+ + +<% if (htmlWebpackPlugin.options.enableGA) { %> + + + +<% } %> + + + diff --git a/src/components/toolbar/index.tsx b/src/components/toolbar/index.tsx index 0165daf3..c067d17c 100644 --- a/src/components/toolbar/index.tsx +++ b/src/components/toolbar/index.tsx @@ -5,7 +5,8 @@ import { useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { StatefulTooltip } from 'baseui/tooltip'; -import { Settings, Minimize2, X } from 'react-feather'; +import { Database, Settings, Minimize2, X } from 'react-feather'; +import { ChampionKeys } from 'src/share/constants/champions'; const Toolbar = () => { const [t] = useTranslation(); @@ -20,6 +21,19 @@ const Toolbar = () => { return (
+ + { + const championId = ChampionKeys[Math.floor(Math.random() * ChampionKeys.length)]; + window.bridge.sendMessage(`showStatistics`, { + championId, + }); + }}> + + + + diff --git a/src/modules/i18n/en-us.ts b/src/modules/i18n/en-us.ts index 442addfa..4085a352 100644 --- a/src/modules/i18n/en-us.ts +++ b/src/modules/i18n/en-us.ts @@ -18,6 +18,7 @@ export default { minimize: `Minimize`, close: `Close`, settings: `Settings`, + statistics: `Statistics`, 'display language': `Display language`, 'select language': `Select language`, 'removed outdated items': `Removed outdated items`, diff --git a/src/modules/i18n/fr-fr.ts b/src/modules/i18n/fr-fr.ts index 52ac0a4a..83638672 100644 --- a/src/modules/i18n/fr-fr.ts +++ b/src/modules/i18n/fr-fr.ts @@ -18,6 +18,7 @@ export default { minimize: `Réduire`, close: `Fermer`, settings: `Paramètres`, + statistics: `Statistiques`, 'display language': `Langue d'affichage`, 'select language': `Sélectionner une langue`, 'removed outdated items': `Anciens objets supprimés`, diff --git a/src/modules/i18n/zh-cn.ts b/src/modules/i18n/zh-cn.ts index f1186220..16028590 100644 --- a/src/modules/i18n/zh-cn.ts +++ b/src/modules/i18n/zh-cn.ts @@ -18,6 +18,7 @@ export default { minimize: `最小化`, close: `退出`, settings: `设置`, + statistics: `英雄数据`, 'display language': `显示语言`, 'select language': `选择`, 'removed outdated items': `已移除旧出装文件`, diff --git a/src/modules/statistics/content.tsx b/src/modules/statistics/content.tsx new file mode 100644 index 00000000..1e377ab7 --- /dev/null +++ b/src/modules/statistics/content.tsx @@ -0,0 +1,90 @@ +import s from './style.module.scss'; + +import React, { useEffect, useState } from 'react'; +import { Scrollbars } from 'react-custom-scrollbars'; +import { QQChampionAvatarPrefix } from 'src/share/constants/sources'; +import Loading from 'src/components/loading-spinner'; +import { IChampionRank } from '@interfaces/commonTypes'; +import { Tabs, Tab } from 'baseui/tabs'; +import { getIChampionRankAsync } from './utils'; +import cn from 'classnames'; + +export function Content() { + const [activeKey, setActiveKey] = useState('0'); + const [championTopRank, setChampionTopRank] = useState([]); + const [championJungleRank, setChampionJungleRank] = useState([]); + const [championMiddleRank, setChampionMiddleRank] = useState([]); + const [championBottomRank, setChampionBottomRank] = useState([]); + const [championSupportRank, setChampionSupportRank] = useState([]); + + async function fetchChampionRank() { + let [top, jungle, middle, bottom, support] = await getIChampionRankAsync(); + setChampionTopRank(top); + setChampionJungleRank(jungle); + setChampionMiddleRank(middle); + setChampionBottomRank(bottom); + setChampionSupportRank(support); + } + + useEffect(() => { + fetchChampionRank(); + }, []); + + const onTabChange = ({ activeKey }: any) => { + setActiveKey(activeKey); + }; + + const renderList = (list: IChampionRank[] = []) => { + const shouldShowList = list.length; + + if (!shouldShowList) { + return ; + } + + return ( + + {list.map((p, idx) => ( +
+
{p.rank}
+
+ {p.rank} +
+
+
{p.id}
+
{p.position}
+
+
{p.winRate}
+
{p.pickRate}
+
+ {p.rank} +
+
+ ))} +
+ ); + }; + + const renderContent = () => { + return ( +
+ + {renderList(championTopRank)} + {renderList(championJungleRank)} + {renderList(championMiddleRank)} + {renderList(championBottomRank)} + {renderList(championSupportRank)} + +
+ ); + }; + + return
{renderContent()}
; +} diff --git a/src/modules/statistics/index.tsx b/src/modules/statistics/index.tsx new file mode 100644 index 00000000..03ba99f2 --- /dev/null +++ b/src/modules/statistics/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { Client as Styletron } from 'styletron-engine-atomic'; +import { Provider as StyletronProvider } from 'styletron-react'; +import { LightTheme, BaseProvider } from 'baseui'; + +import initI18n from 'src/modules/i18n'; +import { Content } from './content'; +import Toolbar from './toolbar'; + +initI18n(); +const engine = new Styletron(); + +export default function Statistics() { + return ( + + + + + + + ); +} diff --git a/src/modules/statistics/style.module.scss b/src/modules/statistics/style.module.scss new file mode 100644 index 00000000..6ed86b0a --- /dev/null +++ b/src/modules/statistics/style.module.scss @@ -0,0 +1,139 @@ +body { + overflow-y: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + margin: unset; + overflow-x: hidden; + background: #ffffff; +} + +.container { + display: flex; + flex-direction: column; + padding-top: 2.7em; + padding-left: 0.1em; + padding-right: 0.1em; +} + +.list { + display: flex; + flex-direction: column; + background-color: #ffffff; +} + +.main { + background-color: #ffffff; +} + +.drag { + align-items: center; + height: 4rem; + padding-left: 2ex; + display: table-row; + vertical-align: inherit; + border-color: inherit; + + .avatar { + pointer-events: none; + -webkit-user-select: none; + -webkit-app-region: drag; + margin-right: 2ex; + height: 2rem; + width: 2rem; + border-radius: 50%; + } + + .cell { + background-color: #fff; + border-top: solid 1 px #e6e6e6; + border-bottom: solid 1 px #e6e6e6; + height: 60 px; + vertical-align: middle; + display: table-cell; + } + + .rank { + line-height: 18px; + font-family: Helvetica, AppleSDGothic, 'Apple SD Gothic Neo', AppleGothic, Arial, Tahoma; + font-size: 16px; + font-weight: 300; + font-style: italic; + text-align: center; + color: #8b8b8b; + } + + .image { + padding-left: 2em; + } + + .champion { + padding-left: 6px; + line-height: 15px; + font-size: 12px; + text-align: left; + color: #b6b6b6; + } + + .value { + padding-left: 8px; + line-height: 14px; + font-family: Helvetica, AppleSDGothic, 'Apple SD Gothic Neo', AppleGothic, Arial, Tahoma; + font-size: 12px; + text-align: center; + color: #b6b6b6; + } + + .tier { + padding-left: 1em; + } +} + +.item { + display: flex; +} + +.loading { + position: fixed; + top: 40vh; + left: calc(50vw - 18px); +} + +.list-loading { + margin: 8em auto; +} + +.toolbar { + width: 100vw; + z-index: 10; + display: flex; + justify-content: flex-end; + position: absolute; + right: 0; + top: 0; + padding-right: 0.7em; + padding-top: 0.7em; + -webkit-user-select: none; + -webkit-app-region: drag; + + &:hover { + cursor: move; + } + + > span { + margin-left: 10px; + } + + .icon { + height: 2em; + width: 2em; + display: flex; + justify-content: center; + align-items: center; + -webkit-app-region: no-drag; + + &:hover { + background-color: #e2e2e2; + cursor: pointer; + } + } +} diff --git a/src/modules/statistics/toolbar.tsx b/src/modules/statistics/toolbar.tsx new file mode 100644 index 00000000..29616e18 --- /dev/null +++ b/src/modules/statistics/toolbar.tsx @@ -0,0 +1,42 @@ +import s from './style.module.scss'; + +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { StatefulTooltip } from 'baseui/tooltip'; +import { X, Lock, Unlock } from 'react-feather'; + +const Toolbar = () => { + const [t] = useTranslation(); + + const [pinned, togglePinned] = useState( + window.bridge.appConfig.get(`statistics.alwaysOnTop`) as boolean, + ); + + const toggleAlwaysOnTop = () => { + window.bridge.sendMessage(`statistics:toggle-always-on-top`); + togglePinned((p) => !p); + }; + + const onClose = () => { + window.bridge.sendMessage(`quit-statistics`); + }; + + return ( +
+ + + {pinned ? : } + + + + + + + + +
+ ); +}; + +export default Toolbar; diff --git a/src/modules/statistics/utils.ts b/src/modules/statistics/utils.ts new file mode 100644 index 00000000..cfa9443a --- /dev/null +++ b/src/modules/statistics/utils.ts @@ -0,0 +1,77 @@ +import { IChampionRank } from '@interfaces/commonTypes'; +const cheerio = require('cheerio'); +const axios = require('axios'); + +async function getIChampionRankAsync_() { + const response = await axios.get('http://www.op.gg/champion/statistics', { + headers: { + 'Content-Language': 'en-US', + 'Accept-Language': 'en-US', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + }, + }); + const $ = cheerio.load(response.data); + let top: IChampionRank[] = fetchChampionRank($, 'tbody.champion-trend-tier-TOP > tr'); + let jungle: IChampionRank[] = fetchChampionRank($, 'tbody.champion-trend-tier-JUNGLE > tr'); + let middle: IChampionRank[] = fetchChampionRank($, 'tbody.champion-trend-tier-MID > tr'); + let bottom: IChampionRank[] = fetchChampionRank($, 'tbody.champion-trend-tier-ADC > tr'); + let support: IChampionRank[] = fetchChampionRank($, 'tbody.champion-trend-tier-SUPPORT > tr'); + return [top, jungle, middle, bottom, support]; +} + +function fetchChampionRank(jqElement: any, selector: string): IChampionRank[] { + let IChampionRank: IChampionRank[] = []; + let CompatibleNaming: { [key: string]: string } = { + 'Nunu & Willump': 'Nunu', + 'Lee Sin': 'LeeSin', + 'Xin Zhao': 'XinZhao', + "Rek'Sai": 'RekSai', + 'Master Yi': 'MasterYi', + "Kha'Zix": 'Khazix', + 'Jarvan IV': 'JarvanIV', + 'Tahm Kench': 'TahmKench', + 'Dr. Mundo': 'DrMundo', + Wukong: 'MonkeyKing', + "Cho'Gath": 'Chogath', + LeBlanc: 'Leblanc', + 'Twisted Fate': 'TwistedFate', + 'Aurelion Sol': 'AurelionSol', + 'Miss Fortune': 'MissFortune', + "Kai'Sa": 'Kaisa', + "Kog'Maw": 'KogMaw', + "Vel'Koz": 'Velkoz', + }; + jqElement(selector).each(function (index: number, element: Element) { + const id: string = jqElement(element) + .children() + .eq(3) + .children() + .eq(0) + .children() + .eq(0) + .text() + .trim(); + IChampionRank.push({ + version: '', + rank: jqElement(element).children().eq(0).text().trim(), + id: CompatibleNaming.hasOwnProperty(id) ? CompatibleNaming[id] : id, + position: jqElement(element) + .children() + .eq(3) + .children() + .eq(0) + .children() + .eq(1) + .text() + .trim() + .replace(new RegExp('\t', 'g'), ''), + winRate: jqElement(element).children().eq(4).text().trim(), + pickRate: jqElement(element).children().eq(5).text().trim(), + tier: jqElement(element).children().eq(6).find('img').eq(0).attr('src'), + }); + }); + return IChampionRank; +} + +export const getIChampionRankAsync = getIChampionRankAsync_; diff --git a/src/statistics.index.tsx b/src/statistics.index.tsx new file mode 100644 index 00000000..30c5196e --- /dev/null +++ b/src/statistics.index.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import Statistics from 'src/modules/statistics'; + +ReactDOM.render(, document.querySelector(`#statistics`));