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.id}
+
{p.position}
+
+
{p.winRate}
+
{p.pickRate}
+
+

+
+
+ ))}
+
+ );
+ };
+
+ 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`));