From 19265e6601d66a139b016638989f287902cbd7e9 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sat, 5 Feb 2022 22:11:43 +0100 Subject: [PATCH 01/35] refactorig --- .env | 5 +- config.json | 7 + discordAuth.json | 7 + discordUrls.json | 4 + electron/config.ts | 35 -- electron/db.ts | 1 - electron/{LCU => features/lcu}/lcu.ts | 28 +- electron/{LCU => features/lcu}/types.ts | 2 +- electron/{ => features}/routes/friends.ts | 42 +- electron/{ => features}/routes/index.ts | 23 +- electron/features/routes/internal.ts | 90 ++++ .../{ => features}/routes/notifications.ts | 10 +- electron/features/store.ts | 158 +++++++ electron/features/ws/discord.ts | 104 +++++ electron/index.ts | 96 ++-- electron/jobs/currentSummonerRank.ts | 119 ++--- electron/jobs/friendListJob.ts | 40 +- electron/selection.ts | 31 -- electron/utils.ts | 12 +- me.json | 14 + package.json | 10 +- src/App.tsx | 1 + src/Home.tsx | 42 +- src/api.ts | 4 + src/components/LCUConnector.tsx | 83 +++- src/components/Navbar.tsx | 2 + src/components/SocketStatus.tsx | 9 + src/features/DevTools/DevTools.tsx | 29 ++ src/features/Discord/Discord.tsx | 429 ++++++++++++++++++ src/features/FriendList/FriendGroup.tsx | 31 +- src/features/FriendList/useFriendList.tsx | 4 +- .../Notifications/NotificationItem.tsx | 17 - src/features/Notifications/Notifications.tsx | 1 + src/features/Options/OptionsPage.tsx | 27 +- src/index.css | 4 +- src/utils.ts | 1 + yarn.lock | 338 ++++++++++---- 37 files changed, 1405 insertions(+), 455 deletions(-) create mode 100644 config.json create mode 100644 discordAuth.json create mode 100644 discordUrls.json delete mode 100644 electron/config.ts rename electron/{LCU => features/lcu}/lcu.ts (87%) rename electron/{LCU => features/lcu}/types.ts (99%) rename electron/{ => features}/routes/friends.ts (81%) rename electron/{ => features}/routes/index.ts (82%) create mode 100644 electron/features/routes/internal.ts rename electron/{ => features}/routes/notifications.ts (90%) create mode 100644 electron/features/store.ts create mode 100644 electron/features/ws/discord.ts delete mode 100644 electron/selection.ts create mode 100644 me.json create mode 100644 src/api.ts create mode 100644 src/components/SocketStatus.tsx create mode 100644 src/features/DevTools/DevTools.tsx create mode 100644 src/features/Discord/Discord.tsx diff --git a/.env b/.env index 5ac52c4..7cffebd 100644 --- a/.env +++ b/.env @@ -12,4 +12,7 @@ TYPEORM_ENTITIES = **/entities/*.js TYPEORM_MIGRATIONS_DIR = migration TYPEORM_MIGRATIONS = migration/*.js -TYPEORM_LOGGING = false \ No newline at end of file +TYPEORM_LOGGING = false + +ELECTRON_WEBPACK_APP_WS_URL = https://back.chainbreak.dev/ws +ELECTRON_WEBPACK_APP_BACKEND_URL = https://back.chainbreak.dev/ \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..e8f6746 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "windowsNotifications": true, + "dirname": "C:\\Users\\Martin\\dev\\lol-stalking\\main\\features", + "defaultLossMessage": "😂😂😂😂😂😂😂😂😂😂😂😂😂", + "socketId": "iwwfhzfS8ZxY", + "accessToken": "jYMrQPaDOqxfrX4xPSSwgGNPD7iBlE" +} \ No newline at end of file diff --git a/discordAuth.json b/discordAuth.json new file mode 100644 index 0000000..cd562a0 --- /dev/null +++ b/discordAuth.json @@ -0,0 +1,7 @@ +{ + "access_token": "jYMrQPaDOqxfrX4xPSSwgGNPD7iBlE", + "expires_in": 604800, + "refresh_token": "Dwac1x7i5nX5r9uA4MTWYeVdLxFIPX", + "scope": "guilds identify", + "token_type": "Bearer" +} \ No newline at end of file diff --git a/discordUrls.json b/discordUrls.json new file mode 100644 index 0000000..20469b9 --- /dev/null +++ b/discordUrls.json @@ -0,0 +1,4 @@ +{ + "inviteUrl": "https://discord.com/api/oauth2/authorize?client_id=939192525133086721&permissions=2147486720&scope=bot", + "authUrl": "https://discord.com/api/oauth2/authorize?response_type=code&client_id=939192525133086721&scope=identify%20guilds&redirect_uri=http://localhost:8080/callback&state=iwwfhzfS8ZxY" +} \ No newline at end of file diff --git a/electron/config.ts b/electron/config.ts deleted file mode 100644 index 41d9356..0000000 --- a/electron/config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fs from "fs/promises"; -import path from "path"; - -export const config: { current: Record | null } = { current: null }; - -const initialConfig = { - windowsNotifications: true, - dirname: __dirname, - defaultLossMessage: - "\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}", -}; -const configFilePath = path.join(__dirname, "config.json"); -export const loadConfig = async () => { - try { - const configArr = JSON.parse(await fs.readFile(configFilePath, "utf-8")); - config.current = configArr; - } catch (e) { - console.log("no config file found, creating it..."); - config.current = { ...initialConfig }; - await persistConfig(); - } finally { - return config.current; - } -}; - -export const editConfig = async (callback: () => void) => { - callback(); - persistConfig(); -}; - -export const persistConfig = () => - config.current && - //@ts-ignore - (console.log(config.current) || - fs.writeFile(configFilePath, JSON.stringify(config.current, null, 4))); diff --git a/electron/db.ts b/electron/db.ts index 16870e6..281fbe9 100644 --- a/electron/db.ts +++ b/electron/db.ts @@ -1,6 +1,5 @@ import isDev from "electron-is-dev"; import path from "path"; -import sqlite3 from "sqlite3"; import { createConnection } from "typeorm"; const dbUrl = path.join(__dirname, "database", "lol-stalker.db"); diff --git a/electron/LCU/lcu.ts b/electron/features/lcu/lcu.ts similarity index 87% rename from electron/LCU/lcu.ts rename to electron/features/lcu/lcu.ts index 8ccc3a4..06d921a 100644 --- a/electron/LCU/lcu.ts +++ b/electron/features/lcu/lcu.ts @@ -2,27 +2,22 @@ import { pick } from "@pastable/core"; import axios, { AxiosInstance } from "axios"; import https from "https"; import LCUConnector from "lcu-connector"; -import { Friend } from "../entities/Friend"; -import { sendInvalidate } from "../routes"; -import { addOrUpdateFriends, getSelectedFriends } from "../routes/friends"; -import { selectedFriends } from "../selection"; -import { sendToClient, Tier } from "../utils"; +import { Friend } from "../../entities/Friend"; +import { sendToClient, Tier } from "../../utils"; import { CurrentSummoner, FriendDto, MatchDto, Queue, RankedStats } from "./types"; +import { editStoreEntry, store } from "../store"; +import { addOrUpdateFriends } from "../routes/friends"; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); export const connector = new LCUConnector(); -export const connectorStatus = { - current: null as any, - api: null as unknown as AxiosInstance, -}; -export const sendConnectorStatus = () => sendToClient("lcu/connection", connectorStatus.current); +export const sendConnectorStatus = () => sendToClient("lcu/connection", store.connectorStatus); connector.on("connect", async (data) => { - connectorStatus.current = data; + editStoreEntry("connectorStatus", data); const { protocol, username, password, address, port } = data; const baseURL = `${protocol}://${username}:${password}@${address}:${port}`; - connectorStatus.api = axios.create({ + store.lcu = axios.create({ baseURL, httpsAgent, headers: { Authorization: `Basic ${data.password}` }, @@ -30,8 +25,7 @@ connector.on("connect", async (data) => { console.log("connected to riot client"); }); connector.on("disconnect", () => { - connectorStatus.current = null; - sendInvalidate("lcuStatus"); + editStoreEntry("connectorStatus", null); }); export interface AuthData { @@ -63,7 +57,7 @@ export const compareFriends = async (oldFriends: FriendStats[], newFriends: Frie ...newFriend, oldFriend, toNotify: !!oldFriend.division, - windowsNotification: selectedFriends.current?.has(newFriend.puuid), + windowsNotification: store.selectedFriends?.has(newFriend.puuid), }); } }); @@ -85,7 +79,7 @@ export const postMessage = (payload: { summonerName: string; message: string }) const url = `/lol-game-client-chat/v1/instant-messages?summonerName=${encodeURI( payload.summonerName )}&message=${encodeURI(payload.message)}`; - return connectorStatus.api.post(url); + return store.lcu?.post(url); }; export const getAllApexLeague = async () => { @@ -153,4 +147,4 @@ export const getSwagger = () => request("/swagger/v2/swagger.json"); type AxiosMethod = "get" | "post" | "put" | "delete" | "patch"; export const request = async (uri: string, method: AxiosMethod = "get") => - (await connectorStatus.api[method](uri)).data as T; + (await store.lcu?.[method](uri))?.data as T; diff --git a/electron/LCU/types.ts b/electron/features/lcu/types.ts similarity index 99% rename from electron/LCU/types.ts rename to electron/features/lcu/types.ts index a49ec1d..f7613f9 100644 --- a/electron/LCU/types.ts +++ b/electron/features/lcu/types.ts @@ -1,4 +1,4 @@ -import { Friend } from "../entities/Friend"; +import { Friend } from "../../entities/Friend"; export interface FriendDto extends Friend { availability: string; diff --git a/electron/routes/friends.ts b/electron/features/routes/friends.ts similarity index 81% rename from electron/routes/friends.ts rename to electron/features/routes/friends.ts index 4b36850..c82ea3d 100644 --- a/electron/routes/friends.ts +++ b/electron/features/routes/friends.ts @@ -2,12 +2,11 @@ import { pick } from "@pastable/core"; import debug from "debug"; import { getManager } from "typeorm"; import { sendFriendList, sendInvalidate } from "."; -import { Friend } from "../entities/Friend"; -import { FriendName } from "../entities/FriendName"; -import { Ranking } from "../entities/Ranking"; -import { FriendDto } from "../LCU/types"; -import { editSelectedFriends, persistSelectedFriends, selectedFriends } from "../selection"; -import { sendToClient } from "../utils"; +import { Friend } from "../../entities/Friend"; +import { FriendName } from "../../entities/FriendName"; +import { Ranking } from "../../entities/Ranking"; +import { FriendDto } from "../lcu/types"; +import { editStoreEntry, store } from "../store"; const friendFields: (keyof FriendDto)[] = [ "gameName", @@ -62,18 +61,19 @@ export const getFriendsAndLastRankingFromDb = async () => { }); }; -export const getSelectedFriends = async () => Array.from(selectedFriends.current!); -export const toggleSelectFriends = async (puuids: Friend["puuid"][], newState: boolean) => - editSelectedFriends(() => - puuids.forEach((puuid) => selectedFriends.current?.[newState ? "add" : "delete"](puuid)) - ); +export const toggleSelectFriends = async (puuids: Friend["puuid"][], newState: boolean) => { + const newSelectedFriends = new Set(store.selectedFriends); + + puuids.forEach((puuid) => newSelectedFriends?.[newState ? "add" : "delete"](puuid)); + await editStoreEntry("selectedFriends", newSelectedFriends); +}; + export const selectAllFriends = async (select: boolean) => { const friends = await getFriendsFromDb(); - return editSelectedFriends(() => - friends.forEach((friend) => - selectedFriends.current?.[select ? "add" : "delete"](friend.puuid) - ) - ); + const newSelectedFriends = new Set(store.selectedFriends); + + friends.forEach((friend) => newSelectedFriends?.[select ? "add" : "delete"](friend.puuid)); + await editStoreEntry("selectedFriends", newSelectedFriends); }; const friendDtoToFriend = (friendDto: FriendDto): Partial => ({ @@ -91,10 +91,6 @@ const friendDtoToFriend = (friendDto: FriendDto): Partial => ({ ]), }); -export const inGameFriends: { current: any[] } = { - current: null as any, -}; - export const addOrUpdateFriends = async (friends: FriendDto[]) => { const existingFriends = await getFriendsFromDb(); const manager = getManager(); @@ -109,7 +105,7 @@ export const addOrUpdateFriends = async (friends: FriendDto[]) => { currentInGame.push({ ...pick(friend, ["puuid", "gameName", "icon"]), ...pick(friend.lol, ["championId", "timeStamp", "gameStatus"]), - }); // championId: friend.lol.championId}); + }); } const friendDto = pick(friend, friendFields); const existingFriend = existingFriends.find((ef) => ef.puuid === friend.puuid); @@ -138,7 +134,8 @@ export const addOrUpdateFriends = async (friends: FriendDto[]) => { await manager.save(manager.create(Friend, friendDtoToFriend(friendDto))); } } - inGameFriends.current = [...currentInGame]; + + await editStoreEntry("inGameFriends", [...currentInGame]); sendInvalidate("friendList/in-game"); sendFriendList(); debug("add or update ended"); @@ -159,7 +156,6 @@ export const friendsApi = { getFriendsAndRankingsFromDb, getFriendAndRankingsFromDb, getFriendsAndLastRankingFromDb, - getSelectedFriends, toggleSelectFriends, addOrUpdateFriends, addRanking, diff --git a/electron/routes/index.ts b/electron/features/routes/index.ts similarity index 82% rename from electron/routes/index.ts rename to electron/features/routes/index.ts index c1b9cef..19abaec 100644 --- a/electron/routes/index.ts +++ b/electron/features/routes/index.ts @@ -1,18 +1,16 @@ -import { config } from "../config"; -import { Friend } from "../entities/Friend"; -import { getAllApexLeague, getMatchHistoryBySummonerPuuid, postMessage } from "../LCU/lcu"; -import { sendToClient } from "../utils"; +import { Friend } from "../../entities/Friend"; +import { sendToClient } from "../../utils"; +import { getAllApexLeague, getMatchHistoryBySummonerPuuid, postMessage } from "../lcu/lcu"; +import { sendStoreEntry, store } from "../store"; import { getFriendAndRankingsFromDb, getFriendsAndLastRankingFromDb, getFriendsAndRankingsFromDb, - getSelectedFriends, selectAllFriends, toggleSelectFriends, } from "./friends"; import { getCursoredNotifications, - getFriendNotifications, getNbNewNotifications, NotificationFilters, setNotificationIsNew, @@ -49,14 +47,9 @@ export const sendNbNewNotifications = async ( sendToClient("notifications/nb-new", nb); }; -export const sendSelected = async () => { - const selected = await getSelectedFriends(); - sendToClient("friendList/selected", selected); -}; - export const sendSelectAllFriends = async (_: any, select: boolean) => { await selectAllFriends(select); - sendSelected(); + sendStoreEntry("selectedFriends"); }; export const sendMatches = async (_: any, puuid: Friend["puuid"]) => { @@ -76,7 +69,7 @@ export const receiveToggleSelectFriends = async ( const payload = Array.isArray(puuids) ? puuids : [puuids]; await toggleSelectFriends(payload, type === "add"); - sendSelected(); + sendStoreEntry("selectedFriends"); }; export const sendApex = async () => { @@ -85,7 +78,7 @@ export const sendApex = async () => { }; export const sendInstantMessage = async (_: any, { summonerName }: { summonerName: string }) => { - if (!config.current) return; - await postMessage({ summonerName, message: config.current.defaultLossMessage }); + if (!store.config) return; + await postMessage({ summonerName, message: store.config.defaultLossMessage }); sendToClient("friendList/message", "ok"); }; diff --git a/electron/features/routes/internal.ts b/electron/features/routes/internal.ts new file mode 100644 index 0000000..4b07192 --- /dev/null +++ b/electron/features/routes/internal.ts @@ -0,0 +1,90 @@ +import { app, ipcMain, IpcMainEvent, shell } from "electron"; +import { + receiveToggleSelectFriends, + sendApex, + sendCursoredNotifications, + sendFriendList, + sendFriendListWithRankings, + sendFriendRank, + sendInstantMessage, + sendMatches, + sendNbNewNotifications, + sendSelectAllFriends, +} from "."; +import { sendToClient, getDbPath } from "../../utils"; +import { getCurrentSummoner, sendConnectorStatus } from "../lcu/lcu"; +import { DiscordUrls, editStoreEntry, sendStore, sendStoreEntry, Store, store } from "../store"; +import { makeSocketClient, sendWs } from "../ws/discord"; + +const getMe = async () => sendToClient("me", await getCurrentSummoner()); +const setConfig: InternalCallback = async (_, data) => { + const newConfig = { ...store.config }; + Object.entries(data).forEach(([key, val]) => (newConfig[key] = val)); + + await editStoreEntry("config", newConfig); +}; +const setStore: InternalCallback = async (_, payload) => { + for (const [key, value] of Object.entries(payload)) { + await editStoreEntry(key as keyof Store, value as any); + } +}; +const passThrough = + (event: string, formattedData?: any): InternalCallback => + (_, data) => + sendWs(event, formattedData || data); + +const getDiscordUrls = async () => { + sendWs("discordUrls"); + store.backendSocket?.once("discordUrls", (data: DiscordUrls) => + editStoreEntry("discordUrls", data) + ); +}; + +const dlDb = () => { + const url = getDbPath(); + shell.showItemInFolder(url); + sendToClient("config/dl-db", "ok"); +}; + +const openExternal: InternalCallback = (_, url: string) => { + shell.openExternal(url); + sendToClient("config/open-external", "ok"); +}; + +type InternalCallback = (event: IpcMainEvent, data: any) => any; +const internalCallbacks: Record = { + "friendList/lastRank": sendFriendList, + "friendList/friend": sendFriendRank, + "friendList/ranks": sendFriendListWithRankings, + "friendList/select": receiveToggleSelectFriends, + "friendList/select-all": sendSelectAllFriends, + "friendList/selected": () => sendStoreEntry("selectedFriends"), + "friendList/in-game": () => sendToClient("friendList/in-game", store.inGameFriends), + "friendList/message": sendInstantMessage, + "notifications/all": sendCursoredNotifications, + "notifications/nb-new": sendNbNewNotifications, + "friend/matches": sendMatches, + "config/apex": sendApex, + "ws/reconnect": () => !store.backendSocket && makeSocketClient(), + config: () => sendToClient("config", store.config), + me: getMe, + "store/set": setStore, + "config/set": setConfig, + "discord/guilds": () => sendWs("guilds", { accessToken: store.discordAuth?.access_token }), + "discord/remove-friends": passThrough("removeSummoners"), + "discord/add-friends": passThrough("addSummoners"), + ws: (_, data) => passThrough(data.event, data.data), + "config/discord-urls": getDiscordUrls, + "config/dl-db": dlDb, + "config/open-external": openExternal, + store: sendStore, +}; + +export const registerInternalRoutes = () => { + Object.entries(internalCallbacks).forEach(([route, cb]) => ipcMain.on(route, cb)); +}; + +ipcMain.on("close", () => { + window.close(); + app.exit(0); +}); diff --git a/electron/routes/notifications.ts b/electron/features/routes/notifications.ts similarity index 90% rename from electron/routes/notifications.ts rename to electron/features/routes/notifications.ts index b38f1ce..bca7c17 100644 --- a/electron/routes/notifications.ts +++ b/electron/features/routes/notifications.ts @@ -1,8 +1,8 @@ import { last } from "@pastable/core"; import { getManager, In, LessThan, MoreThan, SelectQueryBuilder } from "typeorm"; -import { Friend } from "../entities/Friend"; -import { Notification } from "../entities/Notification"; -import { selectedFriends } from "../selection"; +import { Friend } from "../../entities/Friend"; +import { Notification } from "../../entities/Notification"; +import { store } from "../store"; export const addNotification = (data: Partial) => getManager().save(getManager().create(Notification, data)); @@ -36,9 +36,9 @@ const applyFilters = (query: SelectQueryBuilder, filters: Notifica whereClauses.push("notification.id > :currentMaxId"); payload.currentMaxId = filters.currentMaxId; } - if (filters.selected && selectedFriends.current) { + if (filters.selected && store.selectedFriends) { whereClauses.push("friend.puuid IN (:...puuids)"); - payload.puuids = Array.from(selectedFriends.current?.values()); + payload.puuids = Array.from(store.selectedFriends?.values()); } if (filters.types?.length) { whereClauses.push("notification.type IN (:...types)"); diff --git a/electron/features/store.ts b/electron/features/store.ts new file mode 100644 index 0000000..f9f1cfa --- /dev/null +++ b/electron/features/store.ts @@ -0,0 +1,158 @@ +import fs from "fs/promises"; +import { AxiosInstance } from "axios"; +import { connection } from "websocket"; +import { sendToClient } from "../utils"; +import { pick } from "@pastable/core"; + +export const initialConfig = { + windowsNotifications: true, + dirname: __dirname, + defaultLossMessage: + "\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}", +}; + +export interface ConnectorStatus { + address: string; + port: number; + username: string; + password: string; + protocol: string; +} +export interface DiscordAuth { + access_token: string; + expires_in: number; + refresh_token: string; + scope: string; + token_type: string; +} +export type SocketStatus = "initial" | "connecting" | "connected" | "error" | "closed"; +export interface DiscordUrls { + inviteUrl: string; + authUrl: string; +} +export interface Store { + config: Record; + selectedFriends: Set | null; + connectorStatus: null | ConnectorStatus; + lcu: null | AxiosInstance; + inGameFriends: null | any[]; + backendSocket: null | connection; + userGuilds: null | string[]; + discordAuth: null | DiscordAuth; + friends: null | any[]; + socketStatus: SocketStatus; + discordUrls: null | DiscordUrls; + me: any | null; +} + +interface StoreConfig { + persist?: boolean; + notifyOnChange?: boolean; + formatter?: (data: any) => any; + onLoad?: (data: any) => any; +} + +export const store: Store = { + config: initialConfig, + selectedFriends: null, + connectorStatus: null, + lcu: null, + inGameFriends: null, + backendSocket: null, + userGuilds: null, + discordAuth: null, + friends: null, + socketStatus: "initial", + discordUrls: null, + me: null, +}; + +const storeConfig: Partial> = { + config: { + persist: true, + notifyOnChange: true, + }, + discordAuth: { + notifyOnChange: true, + persist: true, + }, + discordUrls: { + notifyOnChange: true, + persist: true, + }, + selectedFriends: { + persist: true, + notifyOnChange: true, + formatter: (data: Set) => Array.from(data), + onLoad: (data: string[]) => new Set(data), + }, + inGameFriends: { + notifyOnChange: true, + }, + userGuilds: { + notifyOnChange: true, + }, + connectorStatus: { + notifyOnChange: true, + }, + socketStatus: { + notifyOnChange: true, + }, + me: { + notifyOnChange: true, + persist: true, + }, +}; + +export const editStoreEntry = async ( + entryName: Entry, + value: Store[Entry] +) => { + const config = storeConfig[entryName]; + console.log("editing", entryName, config); + store[entryName] = value; + + const payload = config?.formatter?.(value) || value; + + if (config?.persist) { + await fs.writeFile(`${entryName}.json`, JSON.stringify(payload, null, 4)); + } + + if (config?.notifyOnChange) { + sendStoreEntry(entryName, payload); + } +}; + +export const sendStoreEntry = (entryName: keyof Store, formattedValue?: any) => { + const payload = formattedValue || getValue(entryName); + sendToClient("store/update", { [entryName]: payload }); +}; + +export const getValue = (entryName: keyof Store) => + storeConfig[entryName]?.formatter?.(store[entryName]) || store[entryName]; + +export const loadStore = async () => { + const persisted = Object.entries(storeConfig) + .filter(([_, config]) => config.persist) + .map(([entryName]) => entryName as keyof Store); + for (const entryName of persisted) { + try { + const stored = JSON.parse(await fs.readFile(`${entryName}.json`, "utf-8")); + if (stored) { + console.log("restoring", entryName, stored); + store[entryName] = storeConfig[entryName]?.onLoad?.(stored) || stored; + } + } catch (e) { + console.log("Couldn't load ", entryName + ".json"); + console.error(e); + } + } +}; + +export const sendStore = () => { + const notified = Object.entries(storeConfig) + .filter(([_, config]) => config.notifyOnChange) + .map(([entryName]) => entryName as keyof Store); + + sendToClient("store", pick(store, notified)); +}; diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts new file mode 100644 index 0000000..8bd627e --- /dev/null +++ b/electron/features/ws/discord.ts @@ -0,0 +1,104 @@ +import { client as WebSocketClient, connection } from "websocket"; +import DiscordOauth2 from "discord-oauth2"; +import { pick } from "@pastable/core"; +import fs from "fs/promises"; +import electronIsDev from "electron-is-dev"; +import { DiscordAuth, editStoreEntry, store } from "../store"; +import { sendToClient } from "../../utils"; +export const makeSocketClient = () => { + const client = new WebSocketClient(); + + client.on("connectFailed", function (error) { + console.log("Connect Error: " + error.toString()); + }); + + client.on("connect", async function (connection) { + console.log("WebSocket Client Connected"); + store.backendSocket = connection; + + await editStoreEntry("socketStatus", "connected"); + + const interval = setInterval(() => sendWs("ping"), 5000); + + if (store.discordAuth?.access_token) { + sendWs("guilds", { accessToken: store.discordAuth.access_token }); + } + + connection.on("error", async function (error) { + console.log("Connection Error: " + error.toString()); + await editStoreEntry("socketStatus", "error"); + }); + + connection.on("close", async function () { + store.backendSocket = null as any as connection; + clearInterval(interval); + console.log("echo-protocol Connection Closed"); + await editStoreEntry("socketStatus", "closed"); + }); + + connection.on("message", function (message) { + if (message.type === "utf8") { + const { event, data } = JSON.parse(message.utf8Data); + console.log("received", event, data); + makeCallback[event]?.(data); + + console.log(store.config.socketId); + } + }); + }); + + console.log( + (electronIsDev ? "http://localhost:8080/ws" : "https://back.chainbreak.dev/ws") + + (store.config.socketId ? `?id=${store.config.socketId}` : "") + ); + console.log(store); + const params = { + ...(store.config.socketId ? { id: store.config.socketId } : {}), + ...(store.discordAuth ? store.discordAuth : {}), + }; + console.log("params", params); + const search = new URLSearchParams( + Object.entries(params).reduce( + (acc, [key, value]) => ({ ...acc, ...(!!value ? { [key]: value } : {}) }), + {} + ) + ); + + client.connect( + (electronIsDev ? "http://localhost:8080/ws" : "https://back.chainbreak.dev/ws") + + "?" + + search.toString(), + "echo-protocol" + ); + + return client; +}; + +export const oauth = new DiscordOauth2(); +const makeCallback: Record void> = { + id: async (data: string) => { + await editStoreEntry("config", { ...store.config, socketId: data }); + }, + guilds: async (guilds: string[]) => { + console.log("guilds", guilds); + await editStoreEntry("userGuilds", guilds); + }, + auth: async (data: DiscordAuth) => { + console.log("logged in", data); + sendWs("guilds", { accessToken: data.access_token }); + await editStoreEntry("discordAuth", data); + }, + me: async (data) => { + console.log("me", data); + await editStoreEntry("me", data); + }, + summoners: async (data) => console.log(data), + "summoners/add": async () => sendWs("guilds", { accessToken: store.discordAuth?.access_token }), + "summoners/remove": async () => + sendWs("guilds", { accessToken: store.discordAuth?.access_token }), + discordUrls: async (data) => editStoreEntry("discordUrls", data), + invalidToken: () => editStoreEntry("discordAuth", null), +}; + +export const sendWs = (event: string, data?: any) => + store.backendSocket?.send(JSON.stringify({ event, data })); diff --git a/electron/index.ts b/electron/index.ts index cc61593..db4d274 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -1,35 +1,22 @@ import dotenv from "dotenv"; -dotenv.config(); -import { app, BrowserWindow, ipcMain, shell } from "electron"; +import { app, BrowserWindow, dialog } from "electron"; import isDev from "electron-is-dev"; -import path, { join } from "path"; -import { config, loadConfig, persistConfig } from "./config"; +import path from "path"; import { makeDb } from "./db"; import { startCheckCurrentSummonerRank } from "./jobs/currentSummonerRank"; import { startCheckFriendListJob } from "./jobs/friendListJob"; -import { connector, sendConnectorStatus } from "./LCU/lcu"; -import { - receiveToggleSelectFriends, - sendApex, - sendCursoredNotifications, - sendFriendList, - sendFriendListWithRankings, - sendFriendRank, - sendInstantMessage, - sendMatches, - sendNbNewNotifications, - sendSelectAllFriends, - sendSelected, -} from "./routes"; -import { inGameFriends } from "./routes/friends"; -import { loadSelectedFriends } from "./selection"; -import { sendToClient } from "./utils"; +import { connector } from "./features/lcu/lcu"; +import { registerInternalRoutes } from "./features/routes/internal"; +import { makeSocketClient } from "./features/ws/discord"; +import { loadStore } from "./features/store"; +dotenv.config(); const height = 600; const width = 1200; const baseBounds = { height, width }; let window: BrowserWindow; + export function makeWindow() { // Create the browser window. window = new BrowserWindow({ @@ -40,26 +27,34 @@ export function makeWindow() { autoHideMenuBar: true, fullscreenable: true, webPreferences: { - preload: join(__dirname, "preload.js"), + preload: path.join(__dirname, "preload.js"), webSecurity: false, allowRunningInsecureContent: true, nodeIntegration: true, }, }); const port = process.env.PORT || 3001; - const url = isDev ? `https://localhost:${port}` : join(__dirname, "../src/out/index.html"); + const url = isDev ? `https://localhost:${port}` : path.join(__dirname, "../src/out/index.html"); // window.webContents.openDevTools(); isDev ? window?.loadURL(url) : window?.loadFile(url); return window; } + +// Create window, load the rest of the app, etc... app.whenReady().then(async () => { await makeDb(); + await loadStore(); connector.start(); - await loadSelectedFriends(); - await loadConfig(); + registerInternalRoutes(); + makeSocketClient(); makeWindow(); + window.webContents.on("will-navigate", function (event, newUrl) { + console.log(newUrl); + // More complex code to handle tokens goes here + }); + startCheckFriendListJob(); startCheckCurrentSummonerRank(); app.on("activate", function () { @@ -71,53 +66,18 @@ app.whenReady().then(async () => { app.quit(); process.exit(0); }); -}); -app.setAppUserModelId("LoL Stalker"); - -ipcMain.on("lcu/connection", () => { - sendConnectorStatus(); -}); -ipcMain.on("friendList/lastRank", sendFriendList); -ipcMain.on("friendList/friend", sendFriendRank); -ipcMain.on("friendList/ranks", sendFriendListWithRankings); -ipcMain.on("friendList/select", receiveToggleSelectFriends); -ipcMain.on("friendList/select-all", sendSelectAllFriends); -ipcMain.on("friendList/selected", () => sendSelected()); -ipcMain.on("friendList/in-game", () => sendToClient("friendList/in-game", inGameFriends.current)); -ipcMain.on("friendList/message", sendInstantMessage); - -ipcMain.on("notifications/all", sendCursoredNotifications); -ipcMain.on("notifications/nb-new", sendNbNewNotifications); -ipcMain.on("friend/matches", sendMatches); - -ipcMain.on("config/apex", sendApex); - -ipcMain.on("config", () => sendToClient("config", config.current)); -ipcMain.on("config/set", async (_, data) => { - Object.entries(data).forEach(([key, val]) => (config.current![key] = val)); - sendToClient("config/set", "ok"); - sendToClient("invalidate", "config"); - await persistConfig(); -}); - -ipcMain.on("config/dl-db", () => { - const url = path.join( - __dirname, - isDev ? "../database/lol-stalker.db" : "./database/lol-stalker.db" - ); - shell.showItemInFolder(url); - sendToClient("config/dl-db", "ok"); + app.on("open-url", (event, url) => { + console.log("oui"); + dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); + }); }); -ipcMain.on("config/open-external", (_, url: string) => { - shell.openExternal(url); - sendToClient("config/open-external", "ok"); +// Handle the protocol. In this case, we choose to show an Error Box. +app.on("open-url", (event, url) => { + dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); }); -ipcMain.on("close", () => { - window.close(); - app.exit(0); -}); +app.setAppUserModelId("LoL Stalker"); app.on("window-all-closed", function () { if (process.platform !== "darwin") app.quit(); diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index 54f5ae1..72dc01f 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -1,55 +1,72 @@ import { pick } from "@pastable/core"; -import { getCurrentSummoner, getSoloQRankedStats } from "../LCU/lcu"; +import { getManager } from "typeorm"; +import { Friend } from "../entities/Friend"; +import { Ranking } from "../entities/Ranking"; +import { getCurrentSummoner, getSoloQRankedStats } from "../features/lcu/lcu"; +import { sendWs } from "../features/ws/discord"; +import { getRankDifference } from "../utils"; export const startCheckCurrentSummonerRank = async () => { - // try { - // const currentSummonerFromLCU = await getCurrentSummoner(); - // const currentSummoner: Prisma.FriendCreateInput = { - // puuid: currentSummonerFromLCU.puuid, - // summonerId: currentSummonerFromLCU.summonerId, - // gameName: currentSummonerFromLCU.displayName, - // name: currentSummonerFromLCU.displayName, - // icon: currentSummonerFromLCU.profileIconId, - // isCurrentSummoner: true, - // }; - // const currentSummonerInDb = await prisma.friend.findUnique({ - // where: { puuid: currentSummoner.puuid }, - // }); - // if (!currentSummonerInDb) { - // console.log("Creating new current summoner in db"); - // await prisma.friend.create({ data: currentSummoner }); - // } - // const summonerRank = await getSoloQRankedStats(currentSummoner.puuid); - // if (!summonerRank) throw `Couldn't find last rank for summoner ${currentSummoner.name}`; - // const lastRankFromDb = await prisma.ranking.findFirst({ - // where: { puuid: currentSummoner.puuid }, - // orderBy: { createdAt: "desc" }, - // }); - // if ( - // !lastRankFromDb || - // lastRankFromDb.tier === summonerRank.tier || - // lastRankFromDb.division === summonerRank.division || - // lastRankFromDb.leaguePoints === summonerRank.leaguePoints - // ) { - // console.log("Last rank from db is different than the new one, inserting new rank..."); - // await prisma.ranking.create({ - // data: { - // ...pick(summonerRank, [ - // "division", - // "tier", - // "leaguePoints", - // "wins", - // "losses", - // "miniSeriesProgress", - // ]), - // puuid: currentSummoner.puuid, - // }, - // }); - // console.log("done!"); - // } else console.log("No change in current summoner rank"); - // setTimeout(() => startCheckCurrentSummonerRank(), 1000 * 60 * 15); - // } catch (e) { - // console.log("something went wrong, retrying in 5s"); - // setTimeout(() => startCheckCurrentSummonerRank(), 5000); - // } + try { + console.log("starting check current summoner"); + const currentSummonerFromLCU = await getCurrentSummoner(); + const summonerRank = await getSoloQRankedStats(currentSummonerFromLCU.puuid); + + if (!summonerRank) throw "no summoner rank found"; + const manager = getManager(); + const summonerInDb = await manager.findOne(Friend, { + where: { puuid: currentSummonerFromLCU.puuid }, + relations: ["rankings"], + }); + // return summonerInDb; + if (!summonerInDb) { + const friend = manager.create(Friend, { + puuid: currentSummonerFromLCU.puuid, + isCurrentSummoner: true, + name: currentSummonerFromLCU.displayName, + gameName: currentSummonerFromLCU.displayName, + icon: currentSummonerFromLCU.profileIconId, + summonerId: currentSummonerFromLCU.summonerId, + }); + await manager.save(friend); + } + const lastRanking = summonerInDb?.rankings.sort( + (a, b) => b.createdAt.getTime() - a.createdAt.getTime() + )[0]; + const friend = new Friend(); + friend.puuid = currentSummonerFromLCU.puuid; + if ( + !lastRanking || + lastRanking.division !== summonerRank.division || + lastRanking.tier !== summonerRank.tier || + lastRanking.leaguePoints !== summonerRank.leaguePoints + ) { + console.log("saving new ranking"); + const ranking = manager.create(Ranking, { + ...pick(summonerRank!, [ + "division", + "leaguePoints", + "tier", + "miniSeriesProgress", + "wins", + "losses", + ]), + friend, + }); + await manager.save(ranking); + if (lastRanking) { + const diff = getRankDifference(lastRanking, ranking); + sendWs("update", { + ...diff, + puuid: currentSummonerFromLCU.puuid, + name: currentSummonerFromLCU.displayName, + }); + } + } + + setTimeout(() => startCheckCurrentSummonerRank(), 1000 * 60); + } catch (e) { + console.log("something went wrong, retrying in 5s", e); + setTimeout(() => startCheckCurrentSummonerRank(), 5000); + } }; diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index 478fbe0..71bebe0 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -1,29 +1,29 @@ import { Notification } from "electron"; -import { config } from "../config"; import { Friend } from "../entities/Friend"; -import { checkFriendList, compareFriends, connectorStatus } from "../LCU/lcu"; -import { addRanking, getFriendsAndLastRankingFromDb } from "../routes/friends"; -import { addNotification } from "../routes/notifications"; +import { checkFriendList, compareFriends } from "../features/lcu/lcu"; import { getRankDifference, sendToClient } from "../utils"; - -export const friendsRef = { - current: null as any, -}; +import { getFriendsAndLastRankingFromDb, addRanking } from "../features/routes/friends"; +import { addNotification } from "../features/routes/notifications"; +import { sendWs } from "../features/ws/discord"; +import { editStoreEntry, store } from "../features/store"; +import { sendInvalidate } from "../features/routes"; export const startCheckFriendListJob = async () => { try { - if (!connectorStatus.current) throw "not connected to LCU"; + if (!store.connectorStatus) throw "not connected to LCU"; console.log("start checking friendlist"); - friendsRef.current = await getFriendsAndLastRankingFromDb(); - - if (!friendsRef.current?.length) { + await editStoreEntry("friends", await getFriendsAndLastRankingFromDb()); + console.log("friends", store.friends?.length); + if (!store.friends?.length) { const friendListStats = await checkFriendList(); - friendsRef.current = friendListStats; + await editStoreEntry("friends", friendListStats); } while (true) { const friendListStats = await checkFriendList(); - const changes = await compareFriends(friendsRef.current, friendListStats); + console.log(store.friends?.length); + const changes = await compareFriends(store.friends!, friendListStats); + await editStoreEntry("friends", friendListStats); if (changes.length) { console.log( `${changes.length} change${changes.length > 1 ? "s" : ""} found in friendList` @@ -36,10 +36,14 @@ export const startCheckFriendListJob = async () => { change.oldFriend as any, change as any ); + sendWs("update", { + ...notification, + puuid: change.puuid, + name: change.name, + }); const friend = new Friend(); friend.puuid = change.puuid; - console.log(config.current); - if (config.current?.windowsNotifications && change.windowsNotification) + if (store.config?.windowsNotifications && change.windowsNotification) new Notification({ title: change.name, body: notification.content, @@ -47,13 +51,11 @@ export const startCheckFriendListJob = async () => { await addNotification({ ...notification, friend }); } } - sendToClient("invalidate", "notifications/nb-new"); + sendInvalidate("notifications/nb-new"); } else { console.log("no soloQ played by friends"); } - friendsRef.current = friendListStats; - await new Promise((resolve) => setTimeout(resolve, 10000)); } } catch (e) { diff --git a/electron/selection.ts b/electron/selection.ts deleted file mode 100644 index 22ad504..0000000 --- a/electron/selection.ts +++ /dev/null @@ -1,31 +0,0 @@ -import fs from "fs/promises"; -import path from "path"; - -const selectedFriendsFilePath = path.join(__dirname, "selectedFriends.json"); -export const selectedFriends: { current: Set | null } = { - current: null, -}; -export const loadSelectedFriends = async () => { - try { - const selectedFriendsArr = JSON.parse(await fs.readFile(selectedFriendsFilePath, "utf-8")); - selectedFriends.current = new Set(selectedFriendsArr); - } catch (e) { - console.log("no selected friends file found, creating it..."); - selectedFriends.current = new Set(); - await persistSelectedFriends(); - } finally { - return selectedFriends.current; - } -}; - -export const editSelectedFriends = async (callback: () => void) => { - callback(); - persistSelectedFriends(); -}; - -export const persistSelectedFriends = () => - selectedFriends.current && - fs.writeFile( - selectedFriendsFilePath, - JSON.stringify(Array.from(selectedFriends.current), null, 4) - ); diff --git a/electron/utils.ts b/electron/utils.ts index 6d57f86..2912a76 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -1,5 +1,7 @@ import debug from "debug"; import { BrowserWindow } from "electron"; +import electronIsDev from "electron-is-dev"; +import path from "path"; import { Ranking } from "./entities/Ranking"; export const sendToClient = (channel: string, ...args: any[]) => @@ -56,8 +58,8 @@ export type Tier = | "CHALLENGER"; type Division = "I" | "II" | "III" | "IV"; interface Rank { - tier: Tier; - division: Division; + tier: string; + division: string; leaguePoints: number; } const tiers = [ @@ -104,3 +106,9 @@ export const getRankDifference = (oldRank: Rank, newRank: Rank) => { content: `${hasLost ? "LOST" : "GAINED"} ${Math.abs(lpDifference)} LP`, }; }; + +export const getDbPath = () => + path.join( + __dirname, + electronIsDev ? "../database/lol-stalker.db" : "./database/lol-stalker.db" + ); diff --git a/me.json b/me.json new file mode 100644 index 0000000..cb2efcb --- /dev/null +++ b/me.json @@ -0,0 +1,14 @@ +{ + "id": "730382245352833115", + "username": "Tinmar", + "avatar": "b1788cb649062c8fc751d94578adbd4c", + "discriminator": "3074", + "public_flags": 0, + "flags": 0, + "banner": null, + "banner_color": null, + "accent_color": null, + "locale": "fr", + "mfa_enabled": false, + "premium_type": 2 +} \ No newline at end of file diff --git a/package.json b/package.json index b1ceb31..ffa0bc6 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,12 @@ "@chakra-ui/react": "^1.6.7", "@emotion/react": "^11", "@emotion/styled": "^11", - "@pastable/core": "^0.1.14", + "@pastable/core": "^0.1.15", "axios": "^0.24.0", "bull": "^4.2.1", "classnames": "^2.3.1", "debug": "^4.3.3", + "discord-oauth2": "^2.9.0", "dotenv": "^14.3.2", "electron-is-dev": "^2.0.0", "framer-motion": "^4", @@ -52,11 +53,15 @@ "react-dom": "^17.0.2", "react-hook-form": "^7.25.3", "react-icons": "^4.3.1", + "react-json-tree": "^0.16.1", "react-query": "^3.34.8", "react-router-dom": "6", "recharts": "^2.1.8", "sqlite3": "^5.0.2", "typeorm": "^0.2.41", + "websocket": "^1.0.34", + "ws": "^8.4.2", + "ws-client-js": "^1.0.5", "yenv": "^3.0.1" }, "devDependencies": { @@ -66,6 +71,8 @@ "@types/react-dom": "^17.0.3", "@types/react-icons": "^3.0.0", "@types/sqlite3": "^3.1.8", + "@types/websocket": "^1.0.5", + "@types/ws": "^8.2.2", "@vitejs/plugin-react": "^1.1.4", "@vitejs/plugin-react-refresh": "^1.3.1", "autoprefixer": "^10.3.1", @@ -76,7 +83,6 @@ "electron-builder": "^22.10.5", "move-cli": "^2.0.0", "postcss": "^8.3.5", - "sqlite3-cli": "^1.0.0", "typescript": "^4.2.3", "vite": "^2.1.2" }, diff --git a/src/App.tsx b/src/App.tsx index 8f60c8d..27e1157 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { ChakraProvider } from "@chakra-ui/react"; import { QueryClient, QueryClientProvider } from "react-query"; import { HashRouter } from "react-router-dom"; import { LCUConnector } from "./components/LCUConnector"; +import { SocketStatus } from "./components/SocketStatus"; import { Home } from "./Home"; import theme from "./theme"; diff --git a/src/Home.tsx b/src/Home.tsx index 6848b56..8f75e55 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -1,13 +1,16 @@ -import { Box, Center, Icon, Spinner } from "@chakra-ui/react"; +import { Box, Center, Flex, Icon, Spinner } from "@chakra-ui/react"; import { useAtomValue } from "jotai/utils"; import { Route, Routes, useLocation } from "react-router-dom"; -import { lcuStatusAtom } from "./components/LCUConnector"; import { Navbar, navbarHeight } from "./components/Navbar"; import { FriendDetails } from "./features/FriendDetails/FriendDetails"; import { FriendList } from "./features/FriendList/FriendList"; import { Notifications } from "./features/Notifications/Notifications"; import { OptionsPage } from "./features/Options/OptionsPage"; import { BiRefresh } from "react-icons/bi"; +import { DevTools } from "./features/DevTools/DevTools"; +import { Discord } from "./features/Discord/Discord"; +import { lcuStatusAtom, Store } from "./components/LCUConnector"; +import { SocketStatus } from "./components/SocketStatus"; export const Home = () => { const lcuStatus = useAtomValue(lcuStatusAtom); @@ -27,25 +30,30 @@ export const Home = () => { - window.location.reload()} + alignItems="center" > - + - + cursor="pointer" + transition="transform .3s" + transitionProperty="transform" + _hover={{ transform: "rotate(90deg)" }} + onClick={() => window.location.reload()} + > + + + ); }; @@ -57,6 +65,8 @@ const AppRoutes = () => { } /> } /> } /> + } /> + } /> ); }; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..6463921 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,4 @@ +import axios from "axios"; +import electronIsDev from "electron-is-dev"; +const baseURL = electronIsDev ? "http://localhost:8080/" : "https://back.chainbreak.dev/"; +export const api = axios.create({ baseURL }); diff --git a/src/components/LCUConnector.tsx b/src/components/LCUConnector.tsx index f8e664c..3811966 100644 --- a/src/components/LCUConnector.tsx +++ b/src/components/LCUConnector.tsx @@ -1,3 +1,4 @@ +import { useInterval } from "@chakra-ui/react"; import axios from "axios"; import { atom } from "jotai"; import { atomWithStorage, useUpdateAtom } from "jotai/utils"; @@ -6,41 +7,97 @@ import { useQuery, useQueryClient } from "react-query"; import { useChampionsList } from "../features/DataDragon/useChampionsList"; import { useItemsList } from "../features/DataDragon/useItemsList"; import { useSummonerSpellsList } from "../features/DataDragon/useSummonerSpellsList"; -import { friendsAtom, selectedFriendsAtom } from "../features/FriendList/useFriendList"; +import { friendsAtom } from "../features/FriendList/useFriendList"; import { AuthData, FriendDto, FriendLastRankDto } from "../types"; import { electronRequest, sendMessage } from "../utils"; -export const lcuStatusAtom = atom(null as unknown as AuthData); - -export enum LocalStorageKeys { - OpenGroups = "lol-stalker/openGroups", +export interface DiscordGuild { + channelId: string; + guildId: string; + name: string; + channelName: string; + nbStalkers: number; + summoners: { id: number; puuid: string; channelId: string; name: string }[]; +} +export interface ConnectorStatus { + address: string; + port: number; + username: string; + password: string; + protocol: string; +} +export interface DiscordAuth { + access_token: string; + expires_in: number; + refresh_token: string; + scope: string; + token_type: string; } -export const openGroupsAtom = atomWithStorage(LocalStorageKeys.OpenGroups, []); +export type SocketStatus = "initial" | "connecting" | "connected" | "error" | "closed"; +export interface DiscordUrls { + inviteUrl: string; + authUrl: string; +} +export interface Me { + id: string; + username: string; + avatar: string; + discriminator: string; + public_flags: number; + flags: number; + banner?: any; + banner_color?: any; + accent_color?: any; + locale: string; + mfa_enabled: boolean; + premium_type: number; +} +export interface Store { + config: Record; + selectedFriends: Array | null; + connectorStatus: null | ConnectorStatus; + inGameFriends: null | any[]; + userGuilds: null | DiscordGuild[]; + discordAuth: null | DiscordAuth; + socketStatus: SocketStatus; + discordUrls: null | DiscordUrls; + me: null | Me; +} -const getLCUStatus = () => electronRequest("lcu/connection"); +export const storeAtom = atom(null); +export const lcuStatusAtom = atom((get) => get(storeAtom)?.connectorStatus); +export const discordGuildsAtom = atom((get) => get(storeAtom)?.userGuilds); +export const configAtom = atom((get) => get(storeAtom)?.config); +export const socketStatusAtom = atom((get) => get(storeAtom)?.socketStatus); +export const selectedFriendsAtom = atom((get) => get(storeAtom)?.selectedFriends); +export const discordAuthAtom = atom((get) => get(storeAtom)?.discordAuth); +export const discordUrlsAtom = atom((get) => get(storeAtom)?.discordUrls); +export const meAtom = atom((get) => get(storeAtom)?.me); export const LCUConnector = () => { const setFriends = useUpdateAtom(friendsAtom); - const setSelectedFriends = useUpdateAtom(selectedFriendsAtom); - const setLcuStatus = useUpdateAtom(lcuStatusAtom); + const setStore = useUpdateAtom(storeAtom); const queryClient = useQueryClient(); - useQuery("lcuStatus", getLCUStatus, { onSuccess: setLcuStatus }); + useQuery("store", () => electronRequest("store"), { + onSuccess: (store) => setStore(store), + }); + usePatchVersion(); useChampionsList(); useItemsList(); useSummonerSpellsList(); useEffect(() => { - window.ipcRenderer.send("lcu/connection"); window.Main.on("invalidate", (queryName: string) => queryClient.invalidateQueries(queryName) ); + window.Main.on("store/update", (data: Partial) => + setStore((store) => (store ? { ...store, ...data } : null)) + ); window.Main.on("friendList/lastRank", (data: FriendLastRankDto[]) => setFriends([...data])); - window.Main.on("friendList/selected", setSelectedFriends); sendMessage("friendList/lastRank"); - sendMessage("friendList/selected"); return () => window.Main.getEventNames().forEach((eventName) => diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 1dedb5d..bc920d4 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -30,6 +30,8 @@ export const Navbar = (props: StackProps) => { Notifications Friendlist Options + Dev tools + Discord {hasSubMenu && (
diff --git a/src/components/SocketStatus.tsx b/src/components/SocketStatus.tsx new file mode 100644 index 0000000..2018f17 --- /dev/null +++ b/src/components/SocketStatus.tsx @@ -0,0 +1,9 @@ +import { Box, BoxProps } from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { socketStatusAtom } from "./LCUConnector"; + +export const SocketStatus = (props: BoxProps) => { + const socketStatus = useAtomValue(socketStatusAtom); + console.log(socketStatus); + return {socketStatus}; +}; diff --git a/src/features/DevTools/DevTools.tsx b/src/features/DevTools/DevTools.tsx new file mode 100644 index 0000000..2247635 --- /dev/null +++ b/src/features/DevTools/DevTools.tsx @@ -0,0 +1,29 @@ +import { Button, Center, Input, Stack, Textarea } from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { useForm } from "react-hook-form"; +import { JSONTree } from "react-json-tree"; +import { storeAtom } from "../../components/LCUConnector"; +import { electronRequest } from "../../utils"; + +export const DevTools = () => { + const store = useAtomValue(storeAtom); + const { handleSubmit, register } = useForm(); + const onSubmit = (data: any) => electronRequest("ws", data); + return ( +
+ + +
+
+ + + +
+
+ + +
+
+
+ ); +}; diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx new file mode 100644 index 0000000..3c206ac --- /dev/null +++ b/src/features/Discord/Discord.tsx @@ -0,0 +1,429 @@ +import { CloseIcon, ExternalLinkIcon } from "@chakra-ui/icons"; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + BoxProps, + Button, + Center, + CenterProps, + chakra, + Divider, + Flex, + Icon, + IconButton, + List, + ListItem, + Modal, + ModalCloseButton, + ModalContent, + Stack, + UnorderedList, + useDisclosure, +} from "@chakra-ui/react"; +import { useSelection } from "@pastable/core"; +import { useAtomValue } from "jotai/utils"; +import { useEffect } from "react"; +import { AiOutlineArrowRight } from "react-icons/ai"; +import { BiLogOut, BiPlusCircle, BiRefresh, BiRightArrow } from "react-icons/bi"; +import { FaDiscord } from "react-icons/fa"; +import { useQuery } from "react-query"; +import { + discordAuthAtom, + DiscordGuild, + discordGuildsAtom, + discordUrlsAtom, + meAtom, + socketStatusAtom, +} from "../../components/LCUConnector"; +import { electronMutation, electronRequest } from "../../utils"; +import { useFriendList } from "../FriendList/useFriendList"; + +const refreshGuilds = () => electronMutation("discord/guilds"); +export const Discord = () => { + const discordAuth = useAtomValue(discordAuthAtom); + const socketStatus = useAtomValue(socketStatusAtom); + + if (socketStatus !== "connected") { + return ( +
+ {socketStatus} +
+ ); + } + + if (!discordAuth) + return ( +
+ +
+ ); + return ( + + + + + + + + ); +}; + +const BotInfos = (props: BoxProps) => { + const guilds = useAtomValue(discordGuildsAtom); + const me = useAtomValue(meAtom); + + if (!guilds) return null; + return ( +
+ + {me && ( + + + + Connected as {me.username} + #{me.discriminator} + + + electronMutation("store/set", { discordAuth: null })} + icon={} + aria-label="Logout" + /> + + )} + + + The bot is active on {guilds?.length} of your servers + + + + Invite the bot to your Discord server + + Send !stalker init in the + channel you want the bot to send messages in + + + Add stalked summoners in the list + + + + +
+ ); +}; + +const BotInvitation = (props: CenterProps) => { + const discordUrls = useAtomValue(discordUrlsAtom); + + if (!discordUrls) return ; + + return ( +
+ + +
+ ); +}; + +const DiscordGuildList = (props: BoxProps) => { + const guilds = useAtomValue(discordGuildsAtom); + + useEffect(() => { + if (!guilds) refreshGuilds(); + }, [guilds]); + + if (!guilds?.length) { + return ( +
+ No guild +
+ ); + } + + return ( + + + + Stalked summoners + + refreshGuilds()} + icon={} + aria-label="Refresh guilds" + /> + + + {guilds.map((guild) => ( + + + + + {guild.name} - {guild.channelName}{" "} + + ({guild.summoners.length}) + + + + {guild.nbStalkers} active stalker + {guild.nbStalkers > 1 ? "s" : ""} + + + + + + {guild.summoners.map((summoner) => ( + + ))} + + + + ))} + + {} + + ); +}; +export const DiscordLoginButton = (props: CenterProps) => { + const discordUrls = useAtomValue(discordUrlsAtom); + console.log(discordUrls); + + useEffect(() => { + if (!discordUrls) electronRequest("config/discord-urls"); + }, [discordUrls]); + + return ( +
+ + +
+ ); +}; +const AddSummonerButton = ({ + guildId, + channelId, + summoners, + name, +}: { + name: string; + guildId: string; + channelId: string; + summoners: DiscordGuild["summoners"]; +}) => { + const disclosure = useDisclosure(); + + return ( + <> +
+
disclosure.onOpen()} userSelect="none"> + + Add summoner +
+
+ + + + + ); +}; + +type SummonerSelection = Omit; +const AddSummonerModal = ({ + summoners, + guildName, + guildId, + channelId, + onClose, +}: { + summoners: DiscordGuild["summoners"]; + guildName: string; + guildId: string; + channelId: string; + onClose: () => void; +}) => { + const { friendGroups } = useFriendList(); + const [selection, api] = useSelection({ + getId: (item) => item.puuid, + }); + + const meQuery = useQuery("me", () => electronRequest("me")); + + if (!friendGroups?.length) + return ( +
+ No friend. You can try refreshing the page (CTRL-R) +
+ ); + + const summonersIds = summoners.map((summoner) => summoner.puuid); + const selectionIds = selection.map((selected) => selected.puuid); + + const onClick = () => { + const { toAdd, toRemove } = selection.reduce( + (acc, current) => + summoners.find((summoner) => summoner.puuid === current.puuid) + ? { ...acc, toRemove: [...acc.toAdd, current] } + : { ...acc, toAdd: [...acc.toAdd, current] }, + { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } + ); + if (toAdd.length) { + console.log("add", toAdd); + window.Main.sendMessage("discord/add-friends", { + channelId, + guildId, + summoners: toAdd, + }); + } + if (toRemove.length) { + console.log("remove", toRemove); + + window.Main.sendMessage("discord/remove-friends", { + channelId, + guildId, + summoners: toRemove.map((summoner) => summoner.puuid), + }); + } + onClose(); + }; + + return ( + + + {selection.length !== 0 && ( +
+ +
+ )} + + Add or remove summoners to {guildName} stalking list + + {meQuery.isSuccess && ( + + api.toggle({ + name: meQuery.data.displayName, + puuid: meQuery.data.puuid, + id: 0, + }) + } + > + Me + + )} + + {friendGroups.map((group) => ( + + {group.groupName} + + + {group.friends.map((friend) => ( + + api.toggle(friend)} + > + {friend.name} + + + ))} + + + + ))} + + {/*
{})}> +
+ + setIsEdit((edit) => !edit)} /> +
+
*/} +
+ ); +}; + +const getBgColor = (initial: string[], selection: string[], puuid: string) => { + if (initial.includes(puuid) && selection.includes(puuid)) return "red.400"; + if (!initial.includes(puuid) && !selection.includes(puuid)) return "initial"; + if (initial.includes(puuid) && !selection.includes(puuid)) return "blue.400"; + if (!initial.includes(puuid) && selection.includes(puuid)) return "green.400"; +}; + +const SummonerPanel = ({ + summoner, + guildId, + channelId, +}: { + summoner: DiscordGuild["summoners"][0]; + guildId: string; + channelId: string; +}) => { + return ( + + {summoner.name} + + electronMutation("discord/remove-friends", { + guildId, + channelId, + summoners: [summoner.puuid], + }) + } + /> + + ); +}; diff --git a/src/features/FriendList/FriendGroup.tsx b/src/features/FriendList/FriendGroup.tsx index 4c03078..71f3896 100644 --- a/src/features/FriendList/FriendGroup.tsx +++ b/src/features/FriendList/FriendGroup.tsx @@ -1,4 +1,3 @@ -import { ChevronDownIcon, ChevronRightIcon } from "@chakra-ui/icons"; import { AccordionButton, AccordionIcon, @@ -8,46 +7,24 @@ import { Checkbox, Flex, FlexProps, - Stack, - useDisclosure, } from "@chakra-ui/react"; -import { useAtom } from "jotai"; import { useAtomValue } from "jotai/utils"; import { ChangeEvent, useMemo } from "react"; import { useNavigate } from "react-router-dom"; -import { openGroupsAtom } from "../../components/LCUConnector"; +import { selectedFriendsAtom } from "../../components/LCUConnector"; import { FriendDto, FriendGroup } from "../../types"; import { sendMessage } from "../../utils"; import { ProfileIcon } from "../DataDragon/Profileicon"; -import { selectedFriendsAtom } from "./useFriendList"; export const FriendGroupRow = ({ group }: { group: FriendGroup }) => { - const [openGroups, setOpenGroups] = useAtom(openGroupsAtom); - const defaultIsOpen = useMemo(() => openGroups.includes(group.groupId), []); const selectedFriends = useAtomValue(selectedFriendsAtom); - const { isOpen, onToggle } = useDisclosure({ - onOpen: () => { - setOpenGroups((openGroups) => { - if (openGroups.includes(group.groupId)) return openGroups; - return [...openGroups, group.groupId]; - }); - }, - onClose: () => { - setOpenGroups((openGroups) => { - if (!openGroups.includes(group.groupId)) return openGroups; - return openGroups.filter((groupId) => groupId !== group.groupId); - }); - }, - defaultIsOpen, - }); - const isChecked = useMemo( - () => group.friends.every((friend) => selectedFriends.includes(friend.puuid)), + () => group.friends.every((friend) => selectedFriends?.includes(friend.puuid)), [group, selectedFriends] ); const isIndeterminate = useMemo( - () => !isChecked && group.friends.some((friend) => selectedFriends.includes(friend.puuid)), + () => !isChecked && group.friends.some((friend) => selectedFriends?.includes(friend.puuid)), [group, selectedFriends] ); @@ -83,7 +60,7 @@ export const FriendGroupRow = ({ group }: { group: FriendGroup }) => { ))} diff --git a/src/features/FriendList/useFriendList.tsx b/src/features/FriendList/useFriendList.tsx index b0d698b..c06bbd6 100644 --- a/src/features/FriendList/useFriendList.tsx +++ b/src/features/FriendList/useFriendList.tsx @@ -1,11 +1,11 @@ import { pick } from "@pastable/core"; import { atom } from "jotai"; import { useAtomValue } from "jotai/utils"; +import { selectedFriendsAtom } from "../../components/LCUConnector"; import { FriendDto, FriendGroup, FriendLastRankDto } from "../../types"; export const friendsAtom = atom([]); export const groupsAtom = atom((get) => getFriendListFilteredByGroups(get(friendsAtom))); -export const selectedFriendsAtom = atom([]); export interface FriendUpdate extends Partial { puuid: FriendDto["puuid"]; } @@ -19,7 +19,7 @@ export const useFriendList = () => { export const useIsFriendSelected = (puuid: FriendDto["puuid"]) => { const selectedFriends = useAtomValue(selectedFriendsAtom); - return selectedFriends.includes(puuid); + return selectedFriends?.includes(puuid); }; export const getFriendListFilteredByGroups = (friends: FriendLastRankDto[]) => { diff --git a/src/features/Notifications/NotificationItem.tsx b/src/features/Notifications/NotificationItem.tsx index da2c934..dd8fe80 100644 --- a/src/features/Notifications/NotificationItem.tsx +++ b/src/features/Notifications/NotificationItem.tsx @@ -75,23 +75,6 @@ export const NotificationItem = ({ }; const formatNotificationContent = (notification: NotificationDto) => { - if (notification.from.slice(0, 4) === "NONE") { - return ( - - CAME BACK FROM - - - ); - } - if (notification.to.slice(0, 4) === "NONE") { - return ( - - WENT TO - - - ); - } - return notification.content.replace(" NA ", " "); }; diff --git a/src/features/Notifications/Notifications.tsx b/src/features/Notifications/Notifications.tsx index e4b43da..67dfb6a 100644 --- a/src/features/Notifications/Notifications.tsx +++ b/src/features/Notifications/Notifications.tsx @@ -8,6 +8,7 @@ import { useNotificationsQueries } from "./useNotificationsQueries"; export const Notifications = () => { const { notificationsQuery, nbNewNotifications } = useNotificationsQueries(); + console.log(notificationsQuery); if (notificationsQuery.isError) return An error has occured; const notificationPages = notificationsQuery.data?.pages; diff --git a/src/features/Options/OptionsPage.tsx b/src/features/Options/OptionsPage.tsx index 508dd88..918ae1a 100644 --- a/src/features/Options/OptionsPage.tsx +++ b/src/features/Options/OptionsPage.tsx @@ -1,6 +1,7 @@ import { Box, Button, + ButtonProps, Center, CenterProps, Checkbox, @@ -16,6 +17,12 @@ import { electronRequest } from "../../utils"; import { AiFillGithub, AiFillTwitterCircle } from "react-icons/ai"; import { CopyIcon } from "@chakra-ui/icons"; import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { api } from "../../../electron/preload"; +import { useAtomValue } from "jotai/utils"; +import { configAtom, discordUrlsAtom } from "../../components/LCUConnector"; +import { useEffect } from "react"; +import { FaDiscord } from "react-icons/fa"; export const OptionsPage = () => { const dlDbMutation = useMutation(() => electronRequest("config/dl-db")); const openExternalBrowserMutation = useMutation((url: string) => @@ -75,21 +82,21 @@ export const OptionsPage = () => { ); }; +const useEditConfigMutation = () => + useMutation((obj: Record) => electronRequest("config/set", obj)); export const ConfigPanel = () => { - const configQuery = useQuery("config", () => electronRequest>("config")); - const editConfigQuery = useMutation((obj: Record) => - electronRequest("config/set", obj) - ); - - if (configQuery.isLoading) return ; - const config = configQuery.data!; + const config = useAtomValue(configAtom); + const editConfigMutation = useEditConfigMutation(); + if (!config) return ; return ( <> editConfigQuery.mutate({ windowsNotifications: e.target.checked })} + onChange={(e) => + editConfigMutation.mutate({ windowsNotifications: e.target.checked }) + } > Windows notifications @@ -104,11 +111,13 @@ export const ConfigPanel = () => { }; export const ConfigForm = ({ config, ...props }: { config: Record } & CenterProps) => { + const editConfigMutation = useEditConfigMutation(); const { handleSubmit, register } = useForm({ defaultValues: { defaultLossMessage: config.defaultLossMessage }, }); - const onSubmit = (data: any) => console.log(data); + const onSubmit = (data: any) => + editConfigMutation.mutate({ defaultLossMessage: data.defaultLossMessage }); return (
diff --git a/src/index.css b/src/index.css index 7a80600..f48e00f 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@200;300;400;500;600;700;800;900&display=swap"); html { box-shadow: none !important; overflow-y: hidden; @@ -22,7 +22,7 @@ html { * { font-family: "Montserrat", sans-serif; - font-weight: 200; + font-weight: 300; } *[data-focus], diff --git a/src/utils.ts b/src/utils.ts index c405c05..5562b5f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import { RankDto } from "./types"; export const sendMessage = window.ipcRenderer.send; const timeoutDelay = 5000; +export const electronMutation = (event: string, data?: any) => window.ipcRenderer.send(event, data); export function electronRequest(event: string, data?: any) { return new Promise((resolve, reject) => { const timeout = setTimeout(reject, timeoutDelay); diff --git a/yarn.lock b/yarn.lock index ca4661c..add712b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -427,6 +427,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.16.7": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.0.tgz#b8d142fc0f7664fb3d9b5833fd40dcbab89276c0" + integrity sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -1249,35 +1256,35 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@pastable/core@^0.1.14": - version "0.1.14" - resolved "https://registry.yarnpkg.com/@pastable/core/-/core-0.1.14.tgz#a9bb4ef057034324fab6ab02adb35585bde2aded" - integrity sha512-8XWyPz18hhG8VLpNbJhbdAOt8B4JGcaVHoHaMS3obGYmjdOOusJNnEjIV0+SOKoMGkDcb16wsQqmhJ2AjOtAAQ== +"@pastable/core@^0.1.15": + version "0.1.15" + resolved "https://registry.yarnpkg.com/@pastable/core/-/core-0.1.15.tgz#1fb8f79b2fad693dfb8874e5a2d1b425be5b0916" + integrity sha512-B90RvNdFbeeO2JJ+87rH23qjtmzqTlX5yr9zgqGK+E432418X4zP+fMYGtxLG7S0MEerVjaxMPgHmaEkG6gyLg== dependencies: - "@pastable/react" "0.1.10" - "@pastable/typings" "0.0.2" - "@pastable/utils" "0.1.9" + "@pastable/react" "0.1.11" + "@pastable/typings" "0.0.3" + "@pastable/utils" "0.1.11" jotai "^0.16.5" react "^17.0.2" -"@pastable/react@0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@pastable/react/-/react-0.1.10.tgz#4f30b25bf10fc377c2370f49fefb071ee8b82165" - integrity sha512-QY49BJepP2Ao5+PcUw1uyhcwunIDG7oUxEU0vBsNJrm5SOVNzAddKIE7aTmomrwheXRVn6j9tyoO+6KpiC8FMg== +"@pastable/react@0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@pastable/react/-/react-0.1.11.tgz#ce5f4898f2ca05ce758dcf494bf2aa9942217e7d" + integrity sha512-GT1W/Vl3mIrZqjjPGvW5xQWqSxJOtfLFroWZY21O+mYYAS/HxZPPZcIW1Kl66d7TLPy17Szu04O0NAM+3NKaaw== dependencies: - "@pastable/utils" "0.1.9" + "@pastable/utils" "0.1.11" jotai "^0.16.8" react "^17.0.2" -"@pastable/typings@0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@pastable/typings/-/typings-0.0.2.tgz#32e71cc3b72edde865101fb54965e3c47912428f" - integrity sha512-e5HAu5CJB0COp+zXHCSIcwQpJE28F6NBN7SYCfaKlJ5Xh3jCMwUbW6KVadnW6lEWxMXVRTrvA13Gy5SMGwijWQ== +"@pastable/typings@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@pastable/typings/-/typings-0.0.3.tgz#01b7f3d76d8c6ad1185c68ae3833e7df0d452184" + integrity sha512-cKB7w9zMD2vFMYSYLM90yZn4pjz0OvY3NRfloqpC+ghIswjhM/JpuOauQBBj2UZH4n7iUSfqZ9NGJTByt6bFYw== -"@pastable/utils@0.1.9": - version "0.1.9" - resolved "https://registry.yarnpkg.com/@pastable/utils/-/utils-0.1.9.tgz#2a67dc657303fce9e7138cd8fcab82be3375563d" - integrity sha512-FDq6bpWwNoE4V1JkSAVI9g6remicTGZRghQXHRrN5DqICxIb+vNt/xy+LsdpuMd3wtqnNClsIGetgyRk/4G3tg== +"@pastable/utils@0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@pastable/utils/-/utils-0.1.11.tgz#b2e5461e5eff0ed1c0beb01125498ca54d7fa7fa" + integrity sha512-UTpWK0/I7WFC2WskT8zGKqth6CwrPWdk+bsVFBJko97lMPuc/3Pvpcro50Ayerdf+2OzLmyKXUuOTKuxUBVnZA== "@popperjs/core@2.4.4": version "2.4.4" @@ -1344,6 +1351,11 @@ dependencies: defer-to-connect "^1.0.1" +"@types/base16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/base16/-/base16-1.0.2.tgz#eb3a07db52309bfefb9ba010dfdb3c0784971f65" + integrity sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg== + "@types/bull@^3.15.7": version "3.15.7" resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.7.tgz#a9d7fb332cc02dc021d0eb234b9604b356e9e6de" @@ -1427,6 +1439,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.173.tgz#9d3b674c67a26cf673756f6aca7b429f237f91ed" integrity sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg== +"@types/lodash@^4.14.178": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + "@types/minimatch@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -1465,7 +1482,7 @@ "@types/node" "*" xmlbuilder ">=11.0.1" -"@types/prop-types@*": +"@types/prop-types@*", "@types/prop-types@^15.7.4": version "15.7.4" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== @@ -1541,6 +1558,20 @@ resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI= +"@types/websocket@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.5.tgz#3fb80ed8e07f88e51961211cd3682a3a4a81569c" + integrity sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ== + dependencies: + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -1878,6 +1909,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" + integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= + base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -1903,14 +1939,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -better-sqlite3@^5.4.3: - version "5.4.3" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-5.4.3.tgz#2cb843ce14c56de9e9c0ca6b89844b7d4b5794b8" - integrity sha512-fPp+8f363qQIhuhLyjI4bu657J/FfMtgiiHKfaTsj3RWDkHlWC1yT7c6kHZDnBxzQVoAINuzg553qKmZ4F1rEw== - dependencies: - integer "^2.1.0" - tar "^4.4.10" - big-integer@^1.6.16: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -2057,6 +2085,13 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bufferutil@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433" + integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw== + dependencies: + node-gyp-build "^4.3.0" + builder-util-runtime@8.7.6: version "8.7.6" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.6.tgz#4b43c96db2bd494ced7694bcd7674934655e8324" @@ -2296,15 +2331,6 @@ cli-truncate@^1.1.0: slice-ansi "^1.0.0" string-width "^2.0.0" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -2344,7 +2370,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -2363,11 +2389,27 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" @@ -2589,6 +2631,11 @@ css-unit-converter@^1.1.1: resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== +csstype@^3.0.10: + version "3.0.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== + csstype@^3.0.2: version "3.0.8" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" @@ -2660,6 +2707,14 @@ d3-shape@^2.0.0: dependencies: d3-array "2" +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2821,6 +2876,11 @@ dir-glob@^2.2.2: dependencies: path-type "^3.0.0" +discord-oauth2@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/discord-oauth2/-/discord-oauth2-2.9.0.tgz#5fb64f79f92f654aa4c7d44e993e4dc257126628" + integrity sha512-2DtocOmsXw5KjhW4eUMpr6RAih+juON0DOzSWr6F8qZePzuOJWznHbqDJmU70YB4Wv2Psyc6Shr3SEvRQ2Gk4Q== + dmg-builder@22.11.7: version "22.11.7" resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-22.11.7.tgz#5956008c18d40ee72c0ea01ffea9590dbf51df89" @@ -2993,11 +3053,37 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.53" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1" + integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.3" + next-tick "~1.0.0" + es6-error@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== +es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + esbuild@^0.12.8: version "0.12.15" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.15.tgz#9d99cf39aeb2188265c5983e983e236829f08af0" @@ -3046,6 +3132,13 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +ext@^1.1.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" + integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== + dependencies: + type "^2.5.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3328,7 +3421,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -3737,11 +3830,6 @@ ini@^1.3.4, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -integer@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/integer/-/integer-2.1.0.tgz#29134ea2f7ba3362ed4dbe6bcca992b1f18ff276" - integrity sha512-vBtiSgrEiNocWvvZX1RVfeOKa2mCHLZQ2p9nkQkQZ/BvEiY+6CcUz0eyjvIiewjJoeNidzg2I+tpPJvpyspL1w== - internmap@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" @@ -3790,6 +3878,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -4184,6 +4277,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.curry@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" + integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= + lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" @@ -4519,6 +4617,11 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== +next-tick@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + node-addon-api@^1.6.3: version "1.7.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" @@ -4529,6 +4632,11 @@ node-addon-api@^3.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-gyp-build@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" + integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== + node-gyp@3.x: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" @@ -4992,6 +5100,15 @@ prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -5049,6 +5166,19 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-base16-styling@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.9.1.tgz#4906b4c0a51636f2dca2cea8b682175aa8bd0c92" + integrity sha512-1s0CY1zRBOQ5M3T61wetEpvQmsYSNtWEcdYzyZNxKa8t7oDvaOn9d21xrGezGAHFWLM7SHcktPuPTrvoqxSfKw== + dependencies: + "@babel/runtime" "^7.16.7" + "@types/base16" "^1.0.2" + "@types/lodash" "^4.14.178" + base16 "^1.0.0" + color "^3.2.1" + csstype "^3.0.10" + lodash.curry "^4.1.1" + react-clientside-effect@^1.2.2: version "1.2.5" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz#e2c4dc3c9ee109f642fac4f5b6e9bf5bcd2219a3" @@ -5092,11 +5222,21 @@ react-icons@*, react-icons@^4.3.1: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== -react-is@^16.10.2, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-json-tree@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.16.1.tgz#dd40a4f759c173873d28e51316c29150e2473de5" + integrity sha512-VYvU/tgekJUrPhEVR6ZC789b3x9QPO7LmRX1YMZfWpqbC8pvXb7D74S0+CE6RVk55bXOS2VH1+8ZWA2PioEyqg== + dependencies: + "@babel/runtime" "^7.16.7" + "@types/prop-types" "^15.7.4" + prop-types "^15.8.1" + react-base16-styling "^0.9.1" + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -5405,11 +5545,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -5575,7 +5710,7 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -5620,6 +5755,13 @@ signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" @@ -5749,14 +5891,6 @@ sprintf-js@^1.1.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== -sqlite3-cli@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/sqlite3-cli/-/sqlite3-cli-1.0.0.tgz#96c971c78f9630cb24e49f3b81bf767a739bf27d" - integrity sha512-piiSLV24Q2odtQlyZ7d1i1JreWMVpVVgaqzrlOh3y6+lFU6PPScQ2BOeJArYX43H4L/ThoIblGiPe+xHOA7a+g== - dependencies: - better-sqlite3 "^5.4.3" - yargs "^15.1.0" - sqlite3@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083" @@ -5953,7 +6087,7 @@ tar@^2.0.0: fstream "^1.0.12" inherits "2" -tar@^4, tar@^4.4.10: +tar@^4: version "4.4.19" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== @@ -6132,6 +6266,16 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.6.0.tgz#3ca6099af5981d36ca86b78442973694278a219f" + integrity sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ== + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -6281,6 +6425,13 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf-8-validate@^5.0.2: + version "5.0.8" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.8.tgz#4a735a61661dbb1c59a0868c397d2fe263f14e58" + integrity sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA== + dependencies: + node-gyp-build "^4.3.0" + utf8-byte-length@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" @@ -6337,10 +6488,17 @@ warning@^4.0.3: dependencies: loose-envify "^1.0.0" -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +websocket@^1.0.34: + version "1.0.34" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" + integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" which@1: version "1.3.1" @@ -6370,15 +6528,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6403,6 +6552,16 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws-client-js@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ws-client-js/-/ws-client-js-1.0.5.tgz#5d2cda0efa52119a1cce147fdc2fbb66e980b8e3" + integrity sha512-zXa8onuqEShX2Wk/zrVOvqz5/++jvq96hiNDRGl63tOLVP5ilvbbOR82baQ4CUqVH2ZJWfbuAx8rAcCoOts6Yg== + +ws@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" + integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -6441,16 +6600,16 @@ xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc= + yallist@^3.0.0, yallist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -6466,7 +6625,7 @@ yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yargs-parser@^18.1.2, yargs-parser@^18.1.3: +yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -6484,23 +6643,6 @@ yargs-parser@^21.0.0: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== -yargs@^15.1.0: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - yargs@^16.0.0, yargs@^16.1.0, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" From 5bb57943d7a19b22f85b4e326e58eb5dcb360ebd Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sat, 5 Feb 2022 22:27:14 +0100 Subject: [PATCH 02/35] fix: store --- config.json | 2 +- electron/features/store.ts | 10 +++++++++- src/features/FriendList/FriendGroup.tsx | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index e8f6746..746ba35 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "windowsNotifications": true, + "windowsNotifications": false, "dirname": "C:\\Users\\Martin\\dev\\lol-stalking\\main\\features", "defaultLossMessage": "😂😂😂😂😂😂😂😂😂😂😂😂😂", "socketId": "iwwfhzfS8ZxY", diff --git a/electron/features/store.ts b/electron/features/store.ts index f9f1cfa..fbe89f5 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -154,5 +154,13 @@ export const sendStore = () => { .filter(([_, config]) => config.notifyOnChange) .map(([entryName]) => entryName as keyof Store); - sendToClient("store", pick(store, notified)); + const payload = notified.reduce( + (acc, entryName) => ({ + ...acc, + [entryName]: storeConfig[entryName]?.formatter?.(store[entryName]) || store[entryName], + }), + {} + ); + + sendToClient("store", payload); }; diff --git a/src/features/FriendList/FriendGroup.tsx b/src/features/FriendList/FriendGroup.tsx index 71f3896..18e44b6 100644 --- a/src/features/FriendList/FriendGroup.tsx +++ b/src/features/FriendList/FriendGroup.tsx @@ -18,7 +18,7 @@ import { ProfileIcon } from "../DataDragon/Profileicon"; export const FriendGroupRow = ({ group }: { group: FriendGroup }) => { const selectedFriends = useAtomValue(selectedFriendsAtom); - + console.log(selectedFriends); const isChecked = useMemo( () => group.friends.every((friend) => selectedFriends?.includes(friend.puuid)), [group, selectedFriends] From 36e41e6f6ad09bff52817748d87c7b9b1f5e52da Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sat, 5 Feb 2022 23:05:28 +0100 Subject: [PATCH 03/35] feat: center friendlist page --- src/features/Discord/Discord.tsx | 16 +++++++++++----- src/features/FriendList/FriendList.tsx | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx index 3c206ac..6e028e2 100644 --- a/src/features/Discord/Discord.tsx +++ b/src/features/Discord/Discord.tsx @@ -28,7 +28,7 @@ import { useSelection } from "@pastable/core"; import { useAtomValue } from "jotai/utils"; import { useEffect } from "react"; import { AiOutlineArrowRight } from "react-icons/ai"; -import { BiLogOut, BiPlusCircle, BiRefresh, BiRightArrow } from "react-icons/bi"; +import { BiFolder, BiLogOut, BiPlusCircle, BiRefresh, BiRightArrow } from "react-icons/bi"; import { FaDiscord } from "react-icons/fa"; import { useQuery } from "react-query"; import { @@ -325,7 +325,7 @@ const AddSummonerModal = ({ }; return ( - + {selection.length !== 0 && (
@@ -334,13 +334,14 @@ const AddSummonerModal = ({
)} - + Add or remove summoners to {guildName} stalking list {meQuery.isSuccess && ( - Me + Me ({meQuery.data.displayName}) )} {friendGroups.map((group) => ( - {group.groupName} + + + + {group.groupName} + + {group.friends.map((friend) => ( diff --git a/src/features/FriendList/FriendList.tsx b/src/features/FriendList/FriendList.tsx index f6aa2f3..f69e282 100644 --- a/src/features/FriendList/FriendList.tsx +++ b/src/features/FriendList/FriendList.tsx @@ -12,7 +12,7 @@ export const FriendList = () => {
); return ( - +
- + {friendGroups?.map((group) => ( ))} - +
); }; From e89670117e7da0c0f5ad400b7b3f0697ac6029cb Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sun, 6 Feb 2022 06:45:54 +0100 Subject: [PATCH 04/35] feat: store --- electron/features/routes/friends.ts | 2 +- electron/features/routes/internal.ts | 16 +- electron/features/store.ts | 31 +++- electron/features/ws/discord.ts | 13 +- electron/index.ts | 5 - electron/jobs/currentSummonerRank.ts | 2 + electron/jobs/friendListJob.ts | 2 - package.json | 4 +- src/Home.tsx | 2 +- src/components/LCUConnector.tsx | 4 +- src/components/Navbar.tsx | 4 +- src/features/DevTools/DevTools.tsx | 24 +-- src/features/Discord/Discord.tsx | 32 ++-- src/features/FriendList/FriendGroup.tsx | 3 +- src/features/FriendList/FriendList.tsx | 103 ++++++++--- src/features/Notifications/InGameFriends.tsx | 17 +- src/features/Notifications/Notifications.tsx | 16 +- src/types.ts | 16 ++ src/utils.ts | 1 - yarn.lock | 169 +------------------ 20 files changed, 198 insertions(+), 268 deletions(-) diff --git a/electron/features/routes/friends.ts b/electron/features/routes/friends.ts index c82ea3d..49d1d30 100644 --- a/electron/features/routes/friends.ts +++ b/electron/features/routes/friends.ts @@ -103,7 +103,7 @@ export const addOrUpdateFriends = async (friends: FriendDto[]) => { friend.lol.gameQueueType === "RANKED_SOLO_5x5" ) { currentInGame.push({ - ...pick(friend, ["puuid", "gameName", "icon"]), + ...pick(friend, ["puuid", "gameName", "icon", "name"]), ...pick(friend.lol, ["championId", "timeStamp", "gameStatus"]), }); } diff --git a/electron/features/routes/internal.ts b/electron/features/routes/internal.ts index 4b07192..3586210 100644 --- a/electron/features/routes/internal.ts +++ b/electron/features/routes/internal.ts @@ -29,9 +29,10 @@ const setStore: InternalCallback = async (_, payload) => { } }; const passThrough = - (event: string, formattedData?: any): InternalCallback => + (event: string, formatter?: (data: any) => any): InternalCallback => (_, data) => - sendWs(event, formattedData || data); + //@ts-ignore + console.log("sending ws", event) || sendWs(event, formatter?.(data) || data); const getDiscordUrls = async () => { sendWs("discordUrls"); @@ -51,6 +52,11 @@ const openExternal: InternalCallback = (_, url: string) => { sendToClient("config/open-external", "ok"); }; +const injectAccessToken = (obj?: any) => ({ + ...(obj || {}), + accessToken: store.discordAuth?.access_token, +}); + type InternalCallback = (event: IpcMainEvent, data: any) => any; const internalCallbacks: Record = { "friendList/lastRank": sendFriendList, @@ -71,9 +77,9 @@ const internalCallbacks: Record = { "store/set": setStore, "config/set": setConfig, "discord/guilds": () => sendWs("guilds", { accessToken: store.discordAuth?.access_token }), - "discord/remove-friends": passThrough("removeSummoners"), - "discord/add-friends": passThrough("addSummoners"), - ws: (_, data) => passThrough(data.event, data.data), + "discord/remove-friends": passThrough("removeSummoners", injectAccessToken), + "discord/add-friends": passThrough("addSummoners", injectAccessToken), + ws: (_, data) => passThrough(data.event, data.data)(_, data), "config/discord-urls": getDiscordUrls, "config/dl-db": dlDb, "config/open-external": openExternal, diff --git a/electron/features/store.ts b/electron/features/store.ts index fbe89f5..a5842f2 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -2,7 +2,9 @@ import fs from "fs/promises"; import { AxiosInstance } from "axios"; import { connection } from "websocket"; import { sendToClient } from "../utils"; -import { pick } from "@pastable/core"; +import path from "path"; +import electronIsDev from "electron-is-dev"; +import { CurrentSummoner } from "./lcu/types"; export const initialConfig = { windowsNotifications: true, @@ -42,6 +44,7 @@ export interface Store { friends: null | any[]; socketStatus: SocketStatus; discordUrls: null | DiscordUrls; + leagueSummoner: null | CurrentSummoner; me: any | null; } @@ -64,6 +67,7 @@ export const store: Store = { friends: null, socketStatus: "initial", discordUrls: null, + leagueSummoner: null, me: null, }; @@ -83,8 +87,8 @@ const storeConfig: Partial> = { selectedFriends: { persist: true, notifyOnChange: true, - formatter: (data: Set) => Array.from(data), - onLoad: (data: string[]) => new Set(data), + formatter: (data: Set) => data && Array.from(data), + onLoad: (data: string[]) => data && new Set(data), }, inGameFriends: { notifyOnChange: true, @@ -102,6 +106,9 @@ const storeConfig: Partial> = { notifyOnChange: true, persist: true, }, + leagueSummoner: { + notifyOnChange: true, + }, }; export const editStoreEntry = async ( @@ -109,13 +116,12 @@ export const editStoreEntry = async ( value: Store[Entry] ) => { const config = storeConfig[entryName]; - console.log("editing", entryName, config); store[entryName] = value; const payload = config?.formatter?.(value) || value; if (config?.persist) { - await fs.writeFile(`${entryName}.json`, JSON.stringify(payload, null, 4)); + await fs.writeFile(getJsonPath(entryName), JSON.stringify(payload, null, 4)); } if (config?.notifyOnChange) { @@ -132,14 +138,21 @@ export const getValue = (entryName: keyof Store) => storeConfig[entryName]?.formatter?.(store[entryName]) || store[entryName]; export const loadStore = async () => { + try { + await fs.stat(jsonFolderPath); + await fs.readdir(jsonFolderPath); + } catch (e) { + console.log(e); + await fs.mkdir(jsonFolderPath); + } + const persisted = Object.entries(storeConfig) .filter(([_, config]) => config.persist) .map(([entryName]) => entryName as keyof Store); for (const entryName of persisted) { try { - const stored = JSON.parse(await fs.readFile(`${entryName}.json`, "utf-8")); + const stored = JSON.parse(await fs.readFile(getJsonPath(entryName), "utf-8")); if (stored) { - console.log("restoring", entryName, stored); store[entryName] = storeConfig[entryName]?.onLoad?.(stored) || stored; } } catch (e) { @@ -148,6 +161,10 @@ export const loadStore = async () => { } } }; +export const getJsonFolder = () => path.join(__dirname, electronIsDev ? "../data" : "../data"); + +const jsonFolderPath = path.join(__dirname, electronIsDev ? "../data" : "./data"); +const getJsonPath = (name: string) => path.join(jsonFolderPath, name + ".json"); export const sendStore = () => { const notified = Object.entries(storeConfig) diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 8bd627e..ba9e229 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -39,24 +39,16 @@ export const makeSocketClient = () => { connection.on("message", function (message) { if (message.type === "utf8") { const { event, data } = JSON.parse(message.utf8Data); - console.log("received", event, data); + console.log("received", event); makeCallback[event]?.(data); - - console.log(store.config.socketId); } }); }); - console.log( - (electronIsDev ? "http://localhost:8080/ws" : "https://back.chainbreak.dev/ws") + - (store.config.socketId ? `?id=${store.config.socketId}` : "") - ); - console.log(store); const params = { ...(store.config.socketId ? { id: store.config.socketId } : {}), ...(store.discordAuth ? store.discordAuth : {}), }; - console.log("params", params); const search = new URLSearchParams( Object.entries(params).reduce( (acc, [key, value]) => ({ ...acc, ...(!!value ? { [key]: value } : {}) }), @@ -80,16 +72,13 @@ const makeCallback: Record void> = { await editStoreEntry("config", { ...store.config, socketId: data }); }, guilds: async (guilds: string[]) => { - console.log("guilds", guilds); await editStoreEntry("userGuilds", guilds); }, auth: async (data: DiscordAuth) => { - console.log("logged in", data); sendWs("guilds", { accessToken: data.access_token }); await editStoreEntry("discordAuth", data); }, me: async (data) => { - console.log("me", data); await editStoreEntry("me", data); }, summoners: async (data) => console.log(data), diff --git a/electron/index.ts b/electron/index.ts index db4d274..d30f25d 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -50,10 +50,6 @@ app.whenReady().then(async () => { registerInternalRoutes(); makeSocketClient(); makeWindow(); - window.webContents.on("will-navigate", function (event, newUrl) { - console.log(newUrl); - // More complex code to handle tokens goes here - }); startCheckFriendListJob(); startCheckCurrentSummonerRank(); @@ -67,7 +63,6 @@ app.whenReady().then(async () => { process.exit(0); }); app.on("open-url", (event, url) => { - console.log("oui"); dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); }); }); diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index 72dc01f..1c713c4 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -3,6 +3,7 @@ import { getManager } from "typeorm"; import { Friend } from "../entities/Friend"; import { Ranking } from "../entities/Ranking"; import { getCurrentSummoner, getSoloQRankedStats } from "../features/lcu/lcu"; +import { editStoreEntry } from "../features/store"; import { sendWs } from "../features/ws/discord"; import { getRankDifference } from "../utils"; @@ -10,6 +11,7 @@ export const startCheckCurrentSummonerRank = async () => { try { console.log("starting check current summoner"); const currentSummonerFromLCU = await getCurrentSummoner(); + await editStoreEntry("leagueSummoner", currentSummonerFromLCU); const summonerRank = await getSoloQRankedStats(currentSummonerFromLCU.puuid); if (!summonerRank) throw "no summoner rank found"; diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index 71bebe0..c037ec7 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -13,7 +13,6 @@ export const startCheckFriendListJob = async () => { if (!store.connectorStatus) throw "not connected to LCU"; console.log("start checking friendlist"); await editStoreEntry("friends", await getFriendsAndLastRankingFromDb()); - console.log("friends", store.friends?.length); if (!store.friends?.length) { const friendListStats = await checkFriendList(); await editStoreEntry("friends", friendListStats); @@ -21,7 +20,6 @@ export const startCheckFriendListJob = async () => { while (true) { const friendListStats = await checkFriendList(); - console.log(store.friends?.length); const changes = await compareFriends(store.friends!, friendListStats); await editStoreEntry("friends", friendListStats); if (changes.length) { diff --git a/package.json b/package.json index ffa0bc6..1fe16db 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "build": "yarn clean && yarn init:db && yarn build:vite && yarn build:electron && yarn migration:up", "build:vite": "vite build", "build:electron": "tsc -p electron", - "dist": "yarn build && electron-builder", + "dist": "set NODE_ENV=PRODUCTION && yarn build && electron-builder", "pack": "yarn build && electron-builder --dir", "clean": "rimraf dist main src/out database", "type-check": "tsc", @@ -40,7 +40,6 @@ "@emotion/styled": "^11", "@pastable/core": "^0.1.15", "axios": "^0.24.0", - "bull": "^4.2.1", "classnames": "^2.3.1", "debug": "^4.3.3", "discord-oauth2": "^2.9.0", @@ -65,7 +64,6 @@ "yenv": "^3.0.1" }, "devDependencies": { - "@types/bull": "^3.15.7", "@types/node": "^16.3.3", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", diff --git a/src/Home.tsx b/src/Home.tsx index 8f75e55..7c1ff83 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -65,7 +65,7 @@ const AppRoutes = () => { } /> } /> } /> - } /> + {process.env.NODE_ENV !== "PRODUCTION" && } />} } /> ); diff --git a/src/components/LCUConnector.tsx b/src/components/LCUConnector.tsx index 3811966..5a08a2a 100644 --- a/src/components/LCUConnector.tsx +++ b/src/components/LCUConnector.tsx @@ -8,7 +8,7 @@ import { useChampionsList } from "../features/DataDragon/useChampionsList"; import { useItemsList } from "../features/DataDragon/useItemsList"; import { useSummonerSpellsList } from "../features/DataDragon/useSummonerSpellsList"; import { friendsAtom } from "../features/FriendList/useFriendList"; -import { AuthData, FriendDto, FriendLastRankDto } from "../types"; +import { AuthData, CurrentSummoner, FriendDto, FriendLastRankDto } from "../types"; import { electronRequest, sendMessage } from "../utils"; export interface DiscordGuild { @@ -62,6 +62,7 @@ export interface Store { discordAuth: null | DiscordAuth; socketStatus: SocketStatus; discordUrls: null | DiscordUrls; + leagueSummoner: null | CurrentSummoner; me: null | Me; } @@ -74,6 +75,7 @@ export const selectedFriendsAtom = atom((get) => get(storeAtom)?.selectedFriends export const discordAuthAtom = atom((get) => get(storeAtom)?.discordAuth); export const discordUrlsAtom = atom((get) => get(storeAtom)?.discordUrls); export const meAtom = atom((get) => get(storeAtom)?.me); +export const leagueSummonerAtom = atom((get) => get(storeAtom)?.leagueSummoner); export const LCUConnector = () => { const setFriends = useUpdateAtom(friendsAtom); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bc920d4..05b4681 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -29,9 +29,9 @@ export const Navbar = (props: StackProps) => { > Notifications Friendlist - Options - Dev tools Discord + Options + {process.env.NODE_ENV !== "PRODUCTION" && Dev tools} {hasSubMenu && (
diff --git a/src/features/DevTools/DevTools.tsx b/src/features/DevTools/DevTools.tsx index 2247635..8bfebc3 100644 --- a/src/features/DevTools/DevTools.tsx +++ b/src/features/DevTools/DevTools.tsx @@ -8,21 +8,21 @@ import { electronRequest } from "../../utils"; export const DevTools = () => { const store = useAtomValue(storeAtom); const { handleSubmit, register } = useForm(); - const onSubmit = (data: any) => electronRequest("ws", data); + const onSubmit = (data: any) => electronRequest("ws", JSON.parse(data)); return (
- - -
- - - -
- - - -
+
+
+ Event: + + Data: + + +
+
+ +
); diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx index 6e028e2..c736283 100644 --- a/src/features/Discord/Discord.tsx +++ b/src/features/Discord/Discord.tsx @@ -88,19 +88,14 @@ const BotInfos = (props: BoxProps) => { > {me && ( - - - - Connected as {me.username} - #{me.discriminator} - - - electronMutation("store/set", { discordAuth: null })} - icon={} - aria-label="Logout" - /> + + + + Connected as + + {me.username} #{me.discriminator} + + )} @@ -159,8 +154,8 @@ const DiscordGuildList = (props: BoxProps) => { return ( - - + + Stalked summoners { - + {guild.name} - {guild.channelName}{" "} ({guild.summoners.length}) @@ -213,7 +208,6 @@ const DiscordGuildList = (props: BoxProps) => { }; export const DiscordLoginButton = (props: CenterProps) => { const discordUrls = useAtomValue(discordUrlsAtom); - console.log(discordUrls); useEffect(() => { if (!discordUrls) electronRequest("config/discord-urls"); @@ -305,7 +299,7 @@ const AddSummonerModal = ({ { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } ); if (toAdd.length) { - console.log("add", toAdd); + // console.log("add", toAdd); window.Main.sendMessage("discord/add-friends", { channelId, guildId, @@ -313,7 +307,7 @@ const AddSummonerModal = ({ }); } if (toRemove.length) { - console.log("remove", toRemove); + // console.log("remove", toRemove); window.Main.sendMessage("discord/remove-friends", { channelId, diff --git a/src/features/FriendList/FriendGroup.tsx b/src/features/FriendList/FriendGroup.tsx index 18e44b6..448f192 100644 --- a/src/features/FriendList/FriendGroup.tsx +++ b/src/features/FriendList/FriendGroup.tsx @@ -18,7 +18,6 @@ import { ProfileIcon } from "../DataDragon/Profileicon"; export const FriendGroupRow = ({ group }: { group: FriendGroup }) => { const selectedFriends = useAtomValue(selectedFriendsAtom); - console.log(selectedFriends); const isChecked = useMemo( () => group.friends.every((friend) => selectedFriends?.includes(friend.puuid)), [group, selectedFriends] @@ -35,7 +34,7 @@ export const FriendGroupRow = ({ group }: { group: FriendGroup }) => { }; return ( - + { const { friendGroups } = useFriendList(); - console.log(friendGroups); + const leagueSummoner = useAtomValue(leagueSummonerAtom); if (!friendGroups?.length) return (
@@ -12,27 +27,71 @@ export const FriendList = () => {
); return ( -
- - - + + + - Unselect all - + {friendGroups?.map((group) => ( + + ))} + - - {friendGroups?.map((group) => ( - - ))} - -
+
); }; diff --git a/src/features/Notifications/InGameFriends.tsx b/src/features/Notifications/InGameFriends.tsx index 5fc945d..cffa65f 100644 --- a/src/features/Notifications/InGameFriends.tsx +++ b/src/features/Notifications/InGameFriends.tsx @@ -1,4 +1,14 @@ -import { Box, BoxProps, Center, chakra, Flex, Spinner, Stack, useInterval } from "@chakra-ui/react"; +import { + Box, + BoxProps, + Center, + chakra, + Divider, + Flex, + Spinner, + Stack, + useInterval, +} from "@chakra-ui/react"; import { useState } from "react"; import { useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; @@ -35,9 +45,10 @@ export const InGameFriends = () => { return ( - + Friend activity + {inGameFriends .sort((a, b) => a.timeStamp - b.timeStamp) .sort( @@ -62,7 +73,7 @@ const InGameFriendRow = ({ friend }: { friend: InGameFriend }) => { onClick={() => navigate(`/friend/${friend.puuid}`)} cursor="pointer" > - {friend.gameName} + {friend.name} diff --git a/src/features/Notifications/Notifications.tsx b/src/features/Notifications/Notifications.tsx index 67dfb6a..b4e2170 100644 --- a/src/features/Notifications/Notifications.tsx +++ b/src/features/Notifications/Notifications.tsx @@ -1,4 +1,4 @@ -import { Box, Center, Flex, Heading, Spinner, Stack } from "@chakra-ui/react"; +import { Box, Center, Divider, Flex, Heading, Spinner, Stack } from "@chakra-ui/react"; import { useCallback, useEffect, useRef } from "react"; import { NotificationDto } from "../../types"; import { InGameFriends } from "./InGameFriends"; @@ -8,7 +8,6 @@ import { useNotificationsQueries } from "./useNotificationsQueries"; export const Notifications = () => { const { notificationsQuery, nbNewNotifications } = useNotificationsQueries(); - console.log(notificationsQuery); if (notificationsQuery.isError) return An error has occured; const notificationPages = notificationsQuery.data?.pages; @@ -16,15 +15,24 @@ export const Notifications = () => { return ( - + + + Filters + + {notificationsQuery.isLoading ? (
) : ( + + Recent notifications + + + {nbNewNotifications && nbNewNotifications > 0 && ( notificationsQuery.refetch()} @@ -46,6 +54,7 @@ export const Notifications = () => { /> )} + @@ -59,6 +68,7 @@ export interface InGameFriend { gameStatus: string; timeStamp: number; puuid: string; + name: string; } export const NotificationContent = ({ diff --git a/src/types.ts b/src/types.ts index ce7fcfb..ab7935f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -76,6 +76,22 @@ export interface ParticipantIdentity { player: Player; } +export interface CurrentSummoner { + accountId: number; + displayName: string; + internalName: string; + nameChangeFlag: boolean; + percentCompleteForNextLevel: number; + privacy: string; + profileIconId: number; + puuid: string; + rerollPoints: any; + summonerId: number; + summonerLevel: number; + unnamed: boolean; + xpSinceLastLevel: number; + xpUntilNextLevel: number; +} export interface Stats { assists: number; causedEarlySurrender: boolean; diff --git a/src/utils.ts b/src/utils.ts index 5562b5f..709350c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -59,7 +59,6 @@ export const makeTierData = (apex: LeagueApex) => { export const getTotalLpFromRank = (rank: RankDto, tierData: TierData) => { let totalLp = 0; - console.log(rank); const tierIndex = tiers.findIndex((tier) => tier === rank.tier); totalLp += rank.leaguePoints; diff --git a/yarn.lock b/yarn.lock index add712b..561a39a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1356,14 +1356,6 @@ resolved "https://registry.yarnpkg.com/@types/base16/-/base16-1.0.2.tgz#eb3a07db52309bfefb9ba010dfdb3c0784971f65" integrity sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg== -"@types/bull@^3.15.7": - version "3.15.7" - resolved "https://registry.yarnpkg.com/@types/bull/-/bull-3.15.7.tgz#a9d7fb332cc02dc021d0eb234b9604b356e9e6de" - integrity sha512-7NC7XN5NoS0A+leJ/dR69ZfKaegOlCZaii/xGgKnCyh1UYisRncibImb7VMwrc3OdJcbDJt6+4om70TeNl3J7g== - dependencies: - "@types/ioredis" "*" - "@types/redis" "^2.8.0" - "@types/d3-color@^2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82" @@ -1420,13 +1412,6 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/ioredis@*": - version "4.28.7" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.7.tgz#ee79987ef80597ba8c17cfbf9345859ff9233c2d" - integrity sha512-jnSGCD2/TPk02j6v6CGqaCEl0LbmLgK6jUuk/AFaSNUBV+SCHiG7E7fnwJreN6hw9GqtLAFkJs4zFbkJrz11mQ== - dependencies: - "@types/node" "*" - "@types/lodash.mergewith@4.6.6": version "4.6.6" resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz#c4698f5b214a433ff35cb2c75ee6ec7f99d79f10" @@ -1519,13 +1504,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/redis@^2.8.0": - version "2.8.32" - resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" - integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== - dependencies: - "@types/node" "*" - "@types/resize-observer-browser@^0.1.6": version "0.1.6" resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz#d8e6c2f830e2650dc06fe74464472ff64b54a302" @@ -2128,20 +2106,6 @@ builder-util@22.11.7: stat-mode "^1.0.0" temp-file "^3.4.0" -bull@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/bull/-/bull-4.2.1.tgz#c5a7e1496c7903274ce90192e4e5cb18f6c866c0" - integrity sha512-YkCQZMOub++siHw3SbYYXZ5xGEn6Tt3BPoCVq/irPNCxUqUYzta8yDlXyyAsfMKMVj0M7PcnynUabfMf9PFpOA== - dependencies: - cron-parser "^2.13.0" - debuglog "^1.0.0" - get-port "^5.1.1" - ioredis "^4.27.0" - lodash "^4.17.21" - p-timeout "^3.2.0" - semver "^7.3.2" - uuid "^8.3.0" - cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2170,14 +2134,6 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -call-bind@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -2347,11 +2303,6 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" -cluster-key-slot@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" - integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== - code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -2597,14 +2548,6 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" -cron-parser@^2.13.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.18.0.tgz#de1bb0ad528c815548371993f81a54e5a089edcf" - integrity sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg== - dependencies: - is-nan "^1.3.0" - moment-timezone "^0.5.31" - cross-spawn@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2755,11 +2698,6 @@ debug@^4.3.3: dependencies: ms "2.1.2" -debuglog@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" - integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= - decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -2839,11 +2777,6 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -denque@^1.1.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -3426,25 +3359,11 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - get-nonce@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== -get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -3642,11 +3561,6 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" -has-symbols@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3842,23 +3756,6 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ioredis@^4.27.0: - version "4.28.3" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.3.tgz#b13fce8a6a7c525ba22e666d72980a3c0ba799aa" - integrity sha512-9JOWVgBnuSxpIgfpjc1OeY1OLmA4t2KOWWURTDRXky+eWO0LZhI33pQNT9gYxANUXfh5p/zYephYni6GPRsksQ== - dependencies: - cluster-key-slot "^1.1.0" - debug "^4.3.1" - denque "^1.1.0" - lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" - lodash.isarguments "^3.1.0" - p-map "^2.1.0" - redis-commands "1.7.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -4004,14 +3901,6 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - is-npm@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" @@ -4282,21 +4171,6 @@ lodash.curry@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -lodash.isarguments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= - lodash.mergewith@4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" @@ -4511,18 +4385,6 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment-timezone@^0.5.31: - version "0.5.34" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" - integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0": - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - move-cli@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/move-cli/-/move-cli-2.0.0.tgz#8228083707b9f3be4818821c2536efcc6cf63c93" @@ -4893,7 +4755,7 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-map@^2.0.0, p-map@^2.1.0: +p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -4905,7 +4767,7 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" -p-timeout@^3.1.0, p-timeout@^3.2.0: +p-timeout@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -5437,23 +5299,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redis-commands@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= - dependencies: - redis-errors "^1.0.0" - reduce-css-calc@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" @@ -5916,11 +5761,6 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -standard-as-callback@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" - integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== - stat-mode@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" @@ -6447,11 +6287,6 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" From 38d2727aabc52cf86bdb132ae69c3392b7f1321d Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sun, 6 Feb 2022 13:01:15 +0100 Subject: [PATCH 05/35] fix: notifications --- electron/features/routes/friends.ts | 13 +++++++++---- electron/jobs/friendListJob.ts | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/electron/features/routes/friends.ts b/electron/features/routes/friends.ts index 49d1d30..5b4cb6f 100644 --- a/electron/features/routes/friends.ts +++ b/electron/features/routes/friends.ts @@ -43,10 +43,15 @@ export const getFriendsAndRankingsFromDb = () => { }; export const getFriendAndRankingsFromDb = (puuid: Friend["puuid"]) => - getManager().findOne(Friend, { - where: { puuid }, - relations: ["rankings", "friendNames", "notifications"], - }); + getManager() + .createQueryBuilder(Friend, "friend") + .leftJoinAndSelect("friend.rankings", "rankings") + .leftJoinAndSelect("friend.friendNames", "friendNames") + .leftJoinAndSelect("friend.notifications", "notifications") + .orderBy("friend.rankings.createdAt", "DESC") + .where("friend.puuid = :puuid", { puuid }) + .getMany(); + export const getFriendsAndLastRankingFromDb = async () => { const friends = await getFriendsAndRankingsFromDb(); return friends.map((friend) => { diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index c037ec7..868fa06 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -21,7 +21,6 @@ export const startCheckFriendListJob = async () => { while (true) { const friendListStats = await checkFriendList(); const changes = await compareFriends(store.friends!, friendListStats); - await editStoreEntry("friends", friendListStats); if (changes.length) { console.log( `${changes.length} change${changes.length > 1 ? "s" : ""} found in friendList` @@ -53,6 +52,7 @@ export const startCheckFriendListJob = async () => { } else { console.log("no soloQ played by friends"); } + await editStoreEntry("friends", friendListStats); await new Promise((resolve) => setTimeout(resolve, 10000)); } From ba81df5c951be4d231e310d31370cc401dcf7f10 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sun, 6 Feb 2022 13:55:16 +0100 Subject: [PATCH 06/35] feat: create db on runtime --- electron/db.ts | 34 +++- electron/features/routes/friends.ts | 4 +- electron/features/store.ts | 8 +- electron/features/ws/discord.ts | 11 +- electron/index.ts | 4 +- package.json | 4 + .../FriendDetails/FriendRankingGraph.tsx | 3 +- src/features/FriendDetails/Profile.tsx | 4 +- src/types.ts | 1 - yarn.lock | 175 +++++++++++++++++- 10 files changed, 222 insertions(+), 26 deletions(-) diff --git a/electron/db.ts b/electron/db.ts index 281fbe9..e0240da 100644 --- a/electron/db.ts +++ b/electron/db.ts @@ -1,15 +1,29 @@ import isDev from "electron-is-dev"; import path from "path"; +import fs from "fs/promises"; +import sqlite3 from "sqlite3"; import { createConnection } from "typeorm"; const dbUrl = path.join(__dirname, "database", "lol-stalker.db"); -// export const db = new sqlite3.Database(dbUrl); - -export const makeDb = () => - isDev - ? createConnection() - : createConnection({ - type: "sqlite", - database: dbUrl, - entities: [path.join(__dirname, "entities/*")], - }); + +export const makeDb = async () => { + if (isDev) return createConnection(); + + const hasDbFile = await fs.stat(dbUrl); + if (!hasDbFile) { + await createDbFile(); + } + + return createConnection({ + type: "sqlite", + database: dbUrl, + entities: [path.join(__dirname, "entities/*")], + migrationsRun: true, + migrations: [path.join(__dirname, "../migration/*")], + }); +}; + +const createDbFile = () => + new Promise((resolve, reject) => { + new sqlite3.Database(dbUrl, (err) => (err ? reject(err) : resolve(true))); + }); diff --git a/electron/features/routes/friends.ts b/electron/features/routes/friends.ts index 5b4cb6f..a468e19 100644 --- a/electron/features/routes/friends.ts +++ b/electron/features/routes/friends.ts @@ -48,9 +48,9 @@ export const getFriendAndRankingsFromDb = (puuid: Friend["puuid"]) => .leftJoinAndSelect("friend.rankings", "rankings") .leftJoinAndSelect("friend.friendNames", "friendNames") .leftJoinAndSelect("friend.notifications", "notifications") - .orderBy("friend.rankings.createdAt", "DESC") + .orderBy("rankings.createdAt", "DESC") .where("friend.puuid = :puuid", { puuid }) - .getMany(); + .getOne(); export const getFriendsAndLastRankingFromDb = async () => { const friends = await getFriendsAndRankingsFromDb(); diff --git a/electron/features/store.ts b/electron/features/store.ts index a5842f2..78dab69 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -27,7 +27,13 @@ export interface DiscordAuth { scope: string; token_type: string; } -export type SocketStatus = "initial" | "connecting" | "connected" | "error" | "closed"; +export type SocketStatus = + | "initial" + | "connecting" + | "connected" + | "error" + | "closed" + | "can't reach server"; export interface DiscordUrls { inviteUrl: string; authUrl: string; diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index ba9e229..8e13c5d 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -5,13 +5,20 @@ import fs from "fs/promises"; import electronIsDev from "electron-is-dev"; import { DiscordAuth, editStoreEntry, store } from "../store"; import { sendToClient } from "../../utils"; -export const makeSocketClient = () => { +export const makeSocketClient = async () => { const client = new WebSocketClient(); - client.on("connectFailed", function (error) { + client.on("connectFailed", async function (error: any) { console.log("Connect Error: " + error.toString()); + console.log(Object.entries(error)); + await editStoreEntry( + "socketStatus", + error.code === "ECONNREFUSED" ? "can't reach server" : "error" + ); }); + await editStoreEntry("socketStatus", "connecting"); + client.on("connect", async function (connection) { console.log("WebSocket Client Connected"); store.backendSocket = connection; diff --git a/electron/index.ts b/electron/index.ts index d30f25d..fdbf612 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -1,5 +1,6 @@ import dotenv from "dotenv"; import { app, BrowserWindow, dialog } from "electron"; +dotenv.config(); import isDev from "electron-is-dev"; import path from "path"; import { makeDb } from "./db"; @@ -9,7 +10,6 @@ import { connector } from "./features/lcu/lcu"; import { registerInternalRoutes } from "./features/routes/internal"; import { makeSocketClient } from "./features/ws/discord"; import { loadStore } from "./features/store"; -dotenv.config(); const height = 600; const width = 1200; @@ -48,7 +48,7 @@ app.whenReady().then(async () => { await loadStore(); connector.start(); registerInternalRoutes(); - makeSocketClient(); + await makeSocketClient(); makeWindow(); startCheckFriendListJob(); diff --git a/package.json b/package.json index 1fe16db..07801ca 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,16 @@ "electron": "^13.1.7", "electron-builder": "^22.10.5", "move-cli": "^2.0.0", + "path-exists-cli": "^2.0.0", "postcss": "^8.3.5", "typescript": "^4.2.3", "vite": "^2.1.2" }, "build": { "asar": false, + "nsis": { + "oneClick": true + }, "files": [ "main", "src/out", diff --git a/src/features/FriendDetails/FriendRankingGraph.tsx b/src/features/FriendDetails/FriendRankingGraph.tsx index 98b1d7b..ba8da0d 100644 --- a/src/features/FriendDetails/FriendRankingGraph.tsx +++ b/src/features/FriendDetails/FriendRankingGraph.tsx @@ -27,10 +27,11 @@ import { formatRank } from "./FriendDetails"; export const FriendRankingGraph = ({ friend }: { friend: FriendDto }) => { const tierDataRef = useRef(null as any); const query = useQuery("config/apex", () => electronRequest("config/apex")); + console.log(friend); const data = useMemo(() => { if (!query.data) return []; tierDataRef.current = makeTierData(query.data); - return friend.rankings.map((rank) => ({ + return friend?.rankings?.map((rank) => ({ ...rank, totalLp: getTotalLpFromRank(rank, tierDataRef.current), })); diff --git a/src/features/FriendDetails/Profile.tsx b/src/features/FriendDetails/Profile.tsx index 873b18a..4f84eb3 100644 --- a/src/features/FriendDetails/Profile.tsx +++ b/src/features/FriendDetails/Profile.tsx @@ -5,7 +5,9 @@ import { ProfileIcon } from "../DataDragon/Profileicon"; import { formatRank } from "./FriendDetails"; export const Profile = ({ friend }: { friend: FriendDto }) => { - const lastRanking = last(friend.rankings); + console.log(friend); + if (!friend?.rankings) return null; + const lastRanking = last(friend?.rankings); return ( diff --git a/src/types.ts b/src/types.ts index ab7935f..e44769c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,7 +30,6 @@ export interface RankDto { losses: number; leaguePoints: number; division: string; - puuid: string; createdAt: Date; } diff --git a/yarn.lock b/yarn.lock index 561a39a..283fa78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1434,7 +1434,7 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== -"@types/minimist@^1.2.0": +"@types/minimist@^1.2.0", "@types/minimist@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== @@ -2153,6 +2153,16 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" +camelcase-keys@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252" + integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg== + dependencies: + camelcase "^6.3.0" + map-obj "^4.1.0" + quick-lru "^5.1.1" + type-fest "^1.2.1" + camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -2163,6 +2173,11 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== +camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001243: version "1.0.30001245" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz#45b941bbd833cb0fa53861ff2bae746b3c6ca5d4" @@ -2711,6 +2726,11 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" + integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== + decimal.js-light@^2.4.1: version "2.5.1" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -3195,6 +3215,14 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + focus-lock@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.8.1.tgz#bb36968abf77a2063fa173cb6c47b12ac8599d33" @@ -3638,6 +3666,13 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + hosted-git-info@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" @@ -3721,6 +3756,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3813,6 +3853,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-core-module@^2.5.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -4166,6 +4213,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.curry@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" @@ -4229,7 +4283,7 @@ map-obj@^1.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^4.0.0: +map-obj@^4.0.0, map-obj@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== @@ -4256,6 +4310,24 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" +meow@^10.1.1: + version "10.1.2" + resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.2.tgz#62951cb69afa69594142c8250806bc30a3912e4d" + integrity sha512-zbuAlN+V/sXlbGchNS9WTWjUzeamwMt/BApKCJi7B0QyZstZaMx0n4Unll/fg0njGtMdC9UP5SAscvOCLYdM+Q== + dependencies: + "@types/minimist" "^1.2.2" + camelcase-keys "^7.0.0" + decamelize "^5.0.0" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.2" + read-pkg-up "^8.0.0" + redent "^4.0.0" + trim-newlines "^4.0.2" + type-fest "^1.2.2" + yargs-parser "^20.2.9" + meow@^6.0.0, meow@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -4324,7 +4396,7 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -min-indent@^1.0.0: +min-indent@^1.0.0, min-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== @@ -4336,7 +4408,7 @@ min-indent@^1.0.0: dependencies: brace-expansion "^1.1.7" -minimist-options@^4.0.2: +minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== @@ -4576,6 +4648,16 @@ normalize-package-data@^2.5.0: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -4748,6 +4830,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -4755,6 +4844,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -4796,7 +4892,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -4833,11 +4929,24 @@ path-dirname@^1.0.0: resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= +path-exists-cli@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-exists-cli/-/path-exists-cli-2.0.0.tgz#cf1963bc5bb88ee789f5e1564a75af18ebfdb97a" + integrity sha512-qGr0A87KYCznmvabblxyxnzA/MtPZ28wH+4SCMP4tjTFAbzqwvs5xpUZExAYzq5OgHe5vIswzdH5iosCb8YF/Q== + dependencies: + meow "^10.1.1" + path-exists "^5.0.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-exists@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" + integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -5011,6 +5120,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -5222,6 +5336,15 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" +read-pkg-up@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-8.0.0.tgz#72f595b65e66110f43b052dd9af4de6b10534670" + integrity sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ== + dependencies: + find-up "^5.0.0" + read-pkg "^6.0.0" + type-fest "^1.0.1" + read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" @@ -5232,6 +5355,16 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +read-pkg@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-6.0.0.tgz#a67a7d6a1c2b0c3cd6aa2ea521f40c458a4a504c" + integrity sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^3.0.2" + parse-json "^5.2.0" + type-fest "^1.0.1" + readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -5299,6 +5432,14 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9" + integrity sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag== + dependencies: + indent-string "^5.0.0" + strip-indent "^4.0.0" + reduce-css-calc@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" @@ -5872,6 +6013,13 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strip-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" + integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA== + dependencies: + min-indent "^1.0.1" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -6052,6 +6200,11 @@ trim-newlines@^3.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +trim-newlines@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.0.2.tgz#d6aaaf6a0df1b4b536d183879a6b939489808c7c" + integrity sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew== + truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -6106,6 +6259,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" @@ -6468,7 +6626,7 @@ yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: +yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -6536,6 +6694,11 @@ yenv@^3.0.1: keyblade "^0.3.2" yargs "^17.2.1" +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zen-observable-ts@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83" From c11f4847c423265403ce2769814e306a92a6ba72 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sun, 6 Feb 2022 15:11:45 +0100 Subject: [PATCH 07/35] feat: store db in userData folder --- .env | 22 ++--- electron/db.ts | 19 +++-- electron/entities/Friend.ts | 3 + electron/features/store.ts | 4 +- .../migration}/1643628930993-dist.js | 0 electron/migration/1644156291716-dist.js | 75 ++++++++++++++++ ormconfig.js | 14 +++ package.json | 13 +-- yarn.lock | 85 +++++++++++++++++++ 9 files changed, 206 insertions(+), 29 deletions(-) rename {migration => electron/migration}/1643628930993-dist.js (100%) create mode 100644 electron/migration/1644156291716-dist.js create mode 100644 ormconfig.js diff --git a/.env b/.env index 7cffebd..a25ace0 100644 --- a/.env +++ b/.env @@ -1,18 +1,18 @@ PORT = 3002 HTTPS = TRUE -TYPEORM_CONNECTION = sqlite -TYPEORM_HOST = localhost -TYPEORM_USERNAME = root -TYPEORM_PASSWORD = admin -TYPEORM_DATABASE = database/lol-stalker.db -TYPEORM_PORT = 3000 -TYPEORM_SYNCHRONIZE = false -TYPEORM_ENTITIES = **/entities/*.js -TYPEORM_MIGRATIONS_DIR = migration -TYPEORM_MIGRATIONS = migration/*.js +#TYPEORM_CONNECTION = sqlite +#TYPEORM_HOST = localhost +#TYPEORM_USERNAME = root +#TYPEORM_PASSWORD = admin +#TYPEORM_DATABASE = $Env:APPDATA/LoL-stalker/database/lol-stalker.dev.db +#TYPEORM_PORT = 3000 +#TYPEORM_SYNCHRONIZE = false +#TYPEORM_ENTITIES = **/entities/*.js +#TYPEORM_MIGRATIONS_DIR = electron/migration +#TYPEORM_MIGRATIONS = migration/*.js -TYPEORM_LOGGING = false +#TYPEORM_LOGGING = false ELECTRON_WEBPACK_APP_WS_URL = https://back.chainbreak.dev/ws ELECTRON_WEBPACK_APP_BACKEND_URL = https://back.chainbreak.dev/ \ No newline at end of file diff --git a/electron/db.ts b/electron/db.ts index e0240da..2ee3998 100644 --- a/electron/db.ts +++ b/electron/db.ts @@ -4,14 +4,21 @@ import fs from "fs/promises"; import sqlite3 from "sqlite3"; import { createConnection } from "typeorm"; -const dbUrl = path.join(__dirname, "database", "lol-stalker.db"); +import { app } from "electron"; +const dbFolder = path.join(app.getPath("userData"), "database"); +const dbUrl = path.join(dbFolder, isDev ? "lol-stalker.dev.db" : "lol-stalker.db"); export const makeDb = async () => { - if (isDev) return createConnection(); - - const hasDbFile = await fs.stat(dbUrl); - if (!hasDbFile) { + try { + await fs.stat(dbFolder); + } catch (e) { + await fs.mkdir(dbFolder); + } + try { + await fs.stat(dbUrl); + } catch (e) { await createDbFile(); + console.log(`db file ${dbUrl} does not exist, creating it`); } return createConnection({ @@ -19,7 +26,7 @@ export const makeDb = async () => { database: dbUrl, entities: [path.join(__dirname, "entities/*")], migrationsRun: true, - migrations: [path.join(__dirname, "../migration/*")], + migrations: [path.join(__dirname, "migration/*")], }); }; diff --git a/electron/entities/Friend.ts b/electron/entities/Friend.ts index 7db6009..ea9e610 100644 --- a/electron/entities/Friend.ts +++ b/electron/entities/Friend.ts @@ -35,6 +35,9 @@ export class Friend { @Column("datetime", { name: "createdAt", default: () => "CURRENT_TIMESTAMP" }) createdAt: Date; + @Column("text", { name: "subscription", nullable: true }) + subscription: string; + @Column("boolean", { name: "isCurrentSummoner", default: () => "false" }) isCurrentSummoner: boolean; diff --git a/electron/features/store.ts b/electron/features/store.ts index 78dab69..1b9e6dc 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -5,6 +5,7 @@ import { sendToClient } from "../utils"; import path from "path"; import electronIsDev from "electron-is-dev"; import { CurrentSummoner } from "./lcu/types"; +import { app } from "electron"; export const initialConfig = { windowsNotifications: true, @@ -167,9 +168,8 @@ export const loadStore = async () => { } } }; -export const getJsonFolder = () => path.join(__dirname, electronIsDev ? "../data" : "../data"); -const jsonFolderPath = path.join(__dirname, electronIsDev ? "../data" : "./data"); +const jsonFolderPath = path.join(app.getPath("userData"), "jsons"); const getJsonPath = (name: string) => path.join(jsonFolderPath, name + ".json"); export const sendStore = () => { diff --git a/migration/1643628930993-dist.js b/electron/migration/1643628930993-dist.js similarity index 100% rename from migration/1643628930993-dist.js rename to electron/migration/1643628930993-dist.js diff --git a/electron/migration/1644156291716-dist.js b/electron/migration/1644156291716-dist.js new file mode 100644 index 0000000..eeb69ad --- /dev/null +++ b/electron/migration/1644156291716-dist.js @@ -0,0 +1,75 @@ +const { MigrationInterface, QueryRunner } = require("typeorm"); + +module.exports = class dist1644156291716 { + name = 'dist1644156291716' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "temporary_Friend" ("puuid" text PRIMARY KEY NOT NULL, "id" text, "gameName" text NOT NULL, "gameTag" text, "groupId" integer NOT NULL DEFAULT (0), "groupName" text NOT NULL DEFAULT ('NONE'), "name" text NOT NULL, "summonerId" integer NOT NULL, "icon" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isCurrentSummoner" boolean NOT NULL DEFAULT (false), "subscription" text)`); + await queryRunner.query(`INSERT INTO "temporary_Friend"("puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner") SELECT "puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner" FROM "Friend"`); + await queryRunner.query(`DROP TABLE "Friend"`); + await queryRunner.query(`ALTER TABLE "temporary_Friend" RENAME TO "Friend"`); + await queryRunner.query(`CREATE TABLE "temporary_Ranking" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "division" text NOT NULL, "tier" text NOT NULL, "leaguePoints" integer NOT NULL, "wins" integer NOT NULL, "losses" integer NOT NULL, "miniSeriesProgress" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text)`); + await queryRunner.query(`INSERT INTO "temporary_Ranking"("id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid") SELECT "id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid" FROM "Ranking"`); + await queryRunner.query(`DROP TABLE "Ranking"`); + await queryRunner.query(`ALTER TABLE "temporary_Ranking" RENAME TO "Ranking"`); + await queryRunner.query(`CREATE TABLE "temporary_FriendName" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text)`); + await queryRunner.query(`INSERT INTO "temporary_FriendName"("id", "name", "createdAt", "puuid") SELECT "id", "name", "createdAt", "puuid" FROM "FriendName"`); + await queryRunner.query(`DROP TABLE "FriendName"`); + await queryRunner.query(`ALTER TABLE "temporary_FriendName" RENAME TO "FriendName"`); + await queryRunner.query(`CREATE TABLE "temporary_Notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" text NOT NULL DEFAULT (''), "from" text NOT NULL, "to" text NOT NULL, "content" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isNew" boolean NOT NULL DEFAULT (true), "puuid" text)`); + await queryRunner.query(`INSERT INTO "temporary_Notification"("id", "type", "from", "to", "content", "createdAt", "isNew", "puuid") SELECT "id", "type", "from", "to", "content", "createdAt", "isNew", "puuid" FROM "Notification"`); + await queryRunner.query(`DROP TABLE "Notification"`); + await queryRunner.query(`ALTER TABLE "temporary_Notification" RENAME TO "Notification"`); + await queryRunner.query(`CREATE TABLE "temporary_Friend" ("puuid" text PRIMARY KEY NOT NULL, "id" text, "gameName" text NOT NULL, "gameTag" text, "groupId" integer NOT NULL DEFAULT (0), "groupName" text NOT NULL DEFAULT ('NONE'), "name" text NOT NULL, "summonerId" integer NOT NULL, "icon" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isCurrentSummoner" boolean NOT NULL DEFAULT (false), "subscription" text, CONSTRAINT "UQ_e9500c8aa0065f96b5e95502506" UNIQUE ("puuid"))`); + await queryRunner.query(`INSERT INTO "temporary_Friend"("puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner", "subscription") SELECT "puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner", "subscription" FROM "Friend"`); + await queryRunner.query(`DROP TABLE "Friend"`); + await queryRunner.query(`ALTER TABLE "temporary_Friend" RENAME TO "Friend"`); + await queryRunner.query(`CREATE TABLE "temporary_Ranking" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "division" text NOT NULL, "tier" text NOT NULL, "leaguePoints" integer NOT NULL, "wins" integer NOT NULL, "losses" integer NOT NULL, "miniSeriesProgress" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text, CONSTRAINT "FK_07dd2b585b204c4f5d9f7e458bf" FOREIGN KEY ("puuid") REFERENCES "Friend" ("puuid") ON DELETE RESTRICT ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "temporary_Ranking"("id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid") SELECT "id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid" FROM "Ranking"`); + await queryRunner.query(`DROP TABLE "Ranking"`); + await queryRunner.query(`ALTER TABLE "temporary_Ranking" RENAME TO "Ranking"`); + await queryRunner.query(`CREATE TABLE "temporary_FriendName" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text, CONSTRAINT "FK_0a11f58ef016f91c3917d0620bb" FOREIGN KEY ("puuid") REFERENCES "Friend" ("puuid") ON DELETE RESTRICT ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "temporary_FriendName"("id", "name", "createdAt", "puuid") SELECT "id", "name", "createdAt", "puuid" FROM "FriendName"`); + await queryRunner.query(`DROP TABLE "FriendName"`); + await queryRunner.query(`ALTER TABLE "temporary_FriendName" RENAME TO "FriendName"`); + await queryRunner.query(`CREATE TABLE "temporary_Notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" text NOT NULL DEFAULT (''), "from" text NOT NULL, "to" text NOT NULL, "content" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isNew" boolean NOT NULL DEFAULT (true), "puuid" text, CONSTRAINT "FK_a0f86a7dba20e7f0d9c708a65a5" FOREIGN KEY ("puuid") REFERENCES "Friend" ("puuid") ON DELETE RESTRICT ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "temporary_Notification"("id", "type", "from", "to", "content", "createdAt", "isNew", "puuid") SELECT "id", "type", "from", "to", "content", "createdAt", "isNew", "puuid" FROM "Notification"`); + await queryRunner.query(`DROP TABLE "Notification"`); + await queryRunner.query(`ALTER TABLE "temporary_Notification" RENAME TO "Notification"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "Notification" RENAME TO "temporary_Notification"`); + await queryRunner.query(`CREATE TABLE "Notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" text NOT NULL DEFAULT (''), "from" text NOT NULL, "to" text NOT NULL, "content" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isNew" boolean NOT NULL DEFAULT (true), "puuid" text)`); + await queryRunner.query(`INSERT INTO "Notification"("id", "type", "from", "to", "content", "createdAt", "isNew", "puuid") SELECT "id", "type", "from", "to", "content", "createdAt", "isNew", "puuid" FROM "temporary_Notification"`); + await queryRunner.query(`DROP TABLE "temporary_Notification"`); + await queryRunner.query(`ALTER TABLE "FriendName" RENAME TO "temporary_FriendName"`); + await queryRunner.query(`CREATE TABLE "FriendName" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text)`); + await queryRunner.query(`INSERT INTO "FriendName"("id", "name", "createdAt", "puuid") SELECT "id", "name", "createdAt", "puuid" FROM "temporary_FriendName"`); + await queryRunner.query(`DROP TABLE "temporary_FriendName"`); + await queryRunner.query(`ALTER TABLE "Ranking" RENAME TO "temporary_Ranking"`); + await queryRunner.query(`CREATE TABLE "Ranking" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "division" text NOT NULL, "tier" text NOT NULL, "leaguePoints" integer NOT NULL, "wins" integer NOT NULL, "losses" integer NOT NULL, "miniSeriesProgress" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text)`); + await queryRunner.query(`INSERT INTO "Ranking"("id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid") SELECT "id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid" FROM "temporary_Ranking"`); + await queryRunner.query(`DROP TABLE "temporary_Ranking"`); + await queryRunner.query(`ALTER TABLE "Friend" RENAME TO "temporary_Friend"`); + await queryRunner.query(`CREATE TABLE "Friend" ("puuid" text PRIMARY KEY NOT NULL, "id" text, "gameName" text NOT NULL, "gameTag" text, "groupId" integer NOT NULL DEFAULT (0), "groupName" text NOT NULL DEFAULT ('NONE'), "name" text NOT NULL, "summonerId" integer NOT NULL, "icon" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isCurrentSummoner" boolean NOT NULL DEFAULT (false), "subscription" text)`); + await queryRunner.query(`INSERT INTO "Friend"("puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner", "subscription") SELECT "puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner", "subscription" FROM "temporary_Friend"`); + await queryRunner.query(`DROP TABLE "temporary_Friend"`); + await queryRunner.query(`ALTER TABLE "Notification" RENAME TO "temporary_Notification"`); + await queryRunner.query(`CREATE TABLE "Notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" text NOT NULL DEFAULT (''), "from" text NOT NULL, "to" text NOT NULL, "content" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isNew" boolean NOT NULL DEFAULT (true), "puuid" text, CONSTRAINT "FK_a0f86a7dba20e7f0d9c708a65a5" FOREIGN KEY ("puuid") REFERENCES "Friend" ("puuid") ON DELETE RESTRICT ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "Notification"("id", "type", "from", "to", "content", "createdAt", "isNew", "puuid") SELECT "id", "type", "from", "to", "content", "createdAt", "isNew", "puuid" FROM "temporary_Notification"`); + await queryRunner.query(`DROP TABLE "temporary_Notification"`); + await queryRunner.query(`ALTER TABLE "FriendName" RENAME TO "temporary_FriendName"`); + await queryRunner.query(`CREATE TABLE "FriendName" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text, CONSTRAINT "FK_0a11f58ef016f91c3917d0620bb" FOREIGN KEY ("puuid") REFERENCES "Friend" ("puuid") ON DELETE RESTRICT ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "FriendName"("id", "name", "createdAt", "puuid") SELECT "id", "name", "createdAt", "puuid" FROM "temporary_FriendName"`); + await queryRunner.query(`DROP TABLE "temporary_FriendName"`); + await queryRunner.query(`ALTER TABLE "Ranking" RENAME TO "temporary_Ranking"`); + await queryRunner.query(`CREATE TABLE "Ranking" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "division" text NOT NULL, "tier" text NOT NULL, "leaguePoints" integer NOT NULL, "wins" integer NOT NULL, "losses" integer NOT NULL, "miniSeriesProgress" text NOT NULL DEFAULT (''), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "puuid" text, CONSTRAINT "FK_07dd2b585b204c4f5d9f7e458bf" FOREIGN KEY ("puuid") REFERENCES "Friend" ("puuid") ON DELETE RESTRICT ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "Ranking"("id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid") SELECT "id", "division", "tier", "leaguePoints", "wins", "losses", "miniSeriesProgress", "createdAt", "puuid" FROM "temporary_Ranking"`); + await queryRunner.query(`DROP TABLE "temporary_Ranking"`); + await queryRunner.query(`ALTER TABLE "Friend" RENAME TO "temporary_Friend"`); + await queryRunner.query(`CREATE TABLE "Friend" ("puuid" text PRIMARY KEY NOT NULL, "id" text, "gameName" text NOT NULL, "gameTag" text, "groupId" integer NOT NULL DEFAULT (0), "groupName" text NOT NULL DEFAULT ('NONE'), "name" text NOT NULL, "summonerId" integer NOT NULL, "icon" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "isCurrentSummoner" boolean NOT NULL DEFAULT (false))`); + await queryRunner.query(`INSERT INTO "Friend"("puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner") SELECT "puuid", "id", "gameName", "gameTag", "groupId", "groupName", "name", "summonerId", "icon", "createdAt", "isCurrentSummoner" FROM "temporary_Friend"`); + await queryRunner.query(`DROP TABLE "temporary_Friend"`); + } +} diff --git a/ormconfig.js b/ormconfig.js new file mode 100644 index 0000000..815a484 --- /dev/null +++ b/ormconfig.js @@ -0,0 +1,14 @@ +const dotenv = require("dotenv"); +const path = require("path"); +dotenv.config(); + +console.log(path.join(__dirname, "electron/migration/*.js")); +module.exports = { + type: "sqlite", + database: path.join(process.env.APPDATA, "LoL Stalker", "database", "lol-stalker.dev.db"), + migrations: [path.join(__dirname, "electron/migration/*.js")], + entities: [path.join(__dirname, "main/entities/*.js")], + cli: { + migrationsDir: "electron/migration", + }, +}; diff --git a/package.json b/package.json index 07801ca..c98e3f2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ }, "productName": "LoL Stalker", "scripts": { - "dev": "yarn migration:up && concurrently \"yarn dev:vite --port 3002\" \" yarn dev:electron\"", + "dev": "concurrently \"yarn dev:vite --port 3002\" \" yarn dev:electron\"", "dev:vite": "vite --https", "dev:electron": "yarn build:electron && electron .", "build": "yarn clean && yarn init:db && yarn build:vite && yarn build:electron && yarn migration:up", @@ -30,7 +30,7 @@ "type-check": "tsc", "init:db": "copyfiles ./base.db ./database && move-cli ./database/base.db ./database/lol-stalker.db", "migrate": "yarn migration:create && yarn migration:up", - "migration:create": "yarn typeorm migration:generate -o -n dist", + "migration:create": "yarn build:electron && yarn typeorm migration:generate -o -n dist", "migration:up": "yarn typeorm migration:run" }, "dependencies": { @@ -92,14 +92,7 @@ }, "files": [ "main", - "src/out", - { - "from": "./database/", - "to": "main/database/", - "filter": [ - "*.db" - ] - } + "src/out" ], "directories": { "buildResources": "resources" diff --git a/yarn.lock b/yarn.lock index 283fa78..4879fe7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1078,6 +1078,18 @@ dependencies: "@chakra-ui/utils" "1.8.2" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -1351,6 +1363,26 @@ dependencies: defer-to-connect "^1.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/base16@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/base16/-/base16-1.0.2.tgz#eb3a07db52309bfefb9ba010dfdb3c0784971f65" @@ -1597,6 +1629,16 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1731,6 +1773,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -2563,6 +2610,11 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2812,6 +2864,11 @@ detect-node@^2.0.4, detect-node@^2.1.0: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-compare@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" @@ -4273,6 +4330,11 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -6212,6 +6274,24 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" +ts-node@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + yn "3.1.1" + tslib@^1.0.0, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -6694,6 +6774,11 @@ yenv@^3.0.1: keyblade "^0.3.2" yargs "^17.2.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 417f8b54cd53d4fe5604e1a1ca03b5a21764e812 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sun, 6 Feb 2022 21:55:58 +0100 Subject: [PATCH 08/35] feat: send all notifications info update --- electron/features/store.ts | 1 - electron/features/ws/discord.ts | 2 +- electron/index.ts | 73 ++++++++++------- electron/jobs/friendListJob.ts | 11 ++- src/api.ts | 2 +- src/features/Discord/Discord.tsx | 86 ++++++++++---------- src/features/Notifications/InGameFriends.tsx | 33 ++++---- 7 files changed, 113 insertions(+), 95 deletions(-) diff --git a/electron/features/store.ts b/electron/features/store.ts index 1b9e6dc..1194b57 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -89,7 +89,6 @@ const storeConfig: Partial> = { }, discordUrls: { notifyOnChange: true, - persist: true, }, selectedFriends: { persist: true, diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 8e13c5d..2b5bda5 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -64,7 +64,7 @@ export const makeSocketClient = async () => { ); client.connect( - (electronIsDev ? "http://localhost:8080/ws" : "https://back.chainbreak.dev/ws") + + (electronIsDev && false ? "http://localhost:8080/ws" : "https://back.chainbreak.dev/ws") + "?" + search.toString(), "echo-protocol" diff --git a/electron/index.ts b/electron/index.ts index fdbf612..0b1212c 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -41,42 +41,55 @@ export function makeWindow() { return window; } +const gotTheLock = app.requestSingleInstanceLock(); -// Create window, load the rest of the app, etc... -app.whenReady().then(async () => { - await makeDb(); - await loadStore(); - connector.start(); - registerInternalRoutes(); - await makeSocketClient(); - makeWindow(); +if (!gotTheLock && !isDev) { + app.quit(); +} else { + if (!isDev) + app.on("second-instance", (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (window) { + if (window.isMinimized()) window.restore(); + window.focus(); + } + }); + // Create window, load the rest of the app, etc... + app.whenReady().then(async () => { + await makeDb(); + await loadStore(); + connector.start(); + registerInternalRoutes(); + await makeSocketClient(); + makeWindow(); - startCheckFriendListJob(); - startCheckCurrentSummonerRank(); - app.on("activate", function () { - // 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 (BrowserWindow.getAllWindows().length === 0) makeWindow(); - }); - app.on("window-all-closed", () => { - app.quit(); - process.exit(0); + startCheckFriendListJob(); + startCheckCurrentSummonerRank(); + app.on("activate", function () { + // 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 (BrowserWindow.getAllWindows().length === 0) makeWindow(); + }); + app.on("window-all-closed", () => { + app.quit(); + process.exit(0); + }); + app.on("open-url", (event, url) => { + dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); + }); }); + + // Handle the protocol. In this case, we choose to show an Error Box. app.on("open-url", (event, url) => { dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); }); -}); - -// Handle the protocol. In this case, we choose to show an Error Box. -app.on("open-url", (event, url) => { - dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); -}); -app.setAppUserModelId("LoL Stalker"); + app.setAppUserModelId("LoL Stalker"); -app.on("window-all-closed", function () { - if (process.platform !== "darwin") app.quit(); -}); + app.on("window-all-closed", function () { + if (process.platform !== "darwin") app.quit(); + }); -app.commandLine.appendSwitch("disable-site-isolation-trials"); -app.commandLine.appendSwitch("ignore-certificate-errors"); + app.commandLine.appendSwitch("disable-site-isolation-trials"); + app.commandLine.appendSwitch("ignore-certificate-errors"); +} diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index 868fa06..67e8da5 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -33,11 +33,18 @@ export const startCheckFriendListJob = async () => { change.oldFriend as any, change as any ); - sendWs("update", { + const payload = { ...notification, + fromDivision: change.oldFriend.division, + fromTier: change.oldFriend.tier, + fromLeaguePoints: change.oldFriend.leaguePoints, + toDivision: change.division, + toTier: change.tier, + toLeaguePoints: change.leaguePoints, puuid: change.puuid, name: change.name, - }); + }; + sendWs("update", payload); const friend = new Friend(); friend.puuid = change.puuid; if (store.config?.windowsNotifications && change.windowsNotification) diff --git a/src/api.ts b/src/api.ts index 6463921..8e2ba3d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,4 @@ import axios from "axios"; import electronIsDev from "electron-is-dev"; -const baseURL = electronIsDev ? "http://localhost:8080/" : "https://back.chainbreak.dev/"; +const baseURL = electronIsDev && false ? "http://localhost:8080/" : "https://back.chainbreak.dev/"; export const api = axios.create({ baseURL }); diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx index c736283..e466e1f 100644 --- a/src/features/Discord/Discord.tsx +++ b/src/features/Discord/Discord.tsx @@ -96,6 +96,12 @@ const BotInfos = (props: BoxProps) => { {me.username} #{me.discriminator}
+ electronMutation("store/set", { discordAuth: null })} + icon={} + aria-label="Logout" + />
)} @@ -144,16 +150,8 @@ const DiscordGuildList = (props: BoxProps) => { if (!guilds) refreshGuilds(); }, [guilds]); - if (!guilds?.length) { - return ( -
- No guild -
- ); - } - return ( - + Stalked summoners @@ -166,41 +164,45 @@ const DiscordGuildList = (props: BoxProps) => { /> - {guilds.map((guild) => ( - - - - - {guild.name} - {guild.channelName}{" "} - - ({guild.summoners.length}) - - - - {guild.nbStalkers} active stalker - {guild.nbStalkers > 1 ? "s" : ""} - - - - - - {guild.summoners.map((summoner) => ( - No guild
+ ) : ( + guilds.map((guild) => ( + + + + + {guild.name} - {guild.channelName}{" "} + + ({guild.summoners.length}) + + + + {guild.nbStalkers} active stalker + {guild.nbStalkers > 1 ? "s" : ""} + + + + + + {guild.summoners.map((summoner) => ( + + ))} + - ))} - - - - ))} + + + )) + )} {}
diff --git a/src/features/Notifications/InGameFriends.tsx b/src/features/Notifications/InGameFriends.tsx index cffa65f..c029119 100644 --- a/src/features/Notifications/InGameFriends.tsx +++ b/src/features/Notifications/InGameFriends.tsx @@ -34,31 +34,28 @@ export const InGameFriends = () => { An error has occured
); - if (!query.data) - return ( -
- No friend activity -
- ); - const inGameFriends = query.data; return ( - + Friend activity - {inGameFriends - .sort((a, b) => a.timeStamp - b.timeStamp) - .sort( - (a, b) => - gameStatusOrder.findIndex((item) => item === a.gameStatus) - - gameStatusOrder.findIndex((item) => item === b.gameStatus) - ) - .map((friend) => ( - - ))} + {!inGameFriends ? ( +
+ No friend activity +
+ ) : ( + inGameFriends + .sort((a, b) => a.timeStamp - b.timeStamp) + .sort( + (a, b) => + gameStatusOrder.findIndex((item) => item === a.gameStatus) - + gameStatusOrder.findIndex((item) => item === b.gameStatus) + ) + .map((friend) => ) + )}
); }; From 63eb9f0d7e152c51424be0cf1400afb2782c26dc Mon Sep 17 00:00:00 2001 From: ledouxm Date: Tue, 8 Feb 2022 18:02:18 +0100 Subject: [PATCH 09/35] minor fixes --- .env | 5 +---- database - Copie/lol-stalker.db | Bin 0 -> 8192 bytes electron/features/routes/friends.ts | 2 ++ electron/features/ws/discord.ts | 9 ++------- electron/jobs/friendListJob.ts | 13 +++++++++++-- electron/utils.ts | 3 +++ src/api.ts | 4 ---- src/features/FriendDetails/FriendDetails.tsx | 2 +- src/features/FriendDetails/FriendMatches.tsx | 6 +++--- .../FriendDetails/FriendRankingGraph.tsx | 2 +- src/features/Notifications/Notifications.tsx | 11 +++++++++-- 11 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 database - Copie/lol-stalker.db delete mode 100644 src/api.ts diff --git a/.env b/.env index a25ace0..ccfd09d 100644 --- a/.env +++ b/.env @@ -12,7 +12,4 @@ HTTPS = TRUE #TYPEORM_MIGRATIONS_DIR = electron/migration #TYPEORM_MIGRATIONS = migration/*.js -#TYPEORM_LOGGING = false - -ELECTRON_WEBPACK_APP_WS_URL = https://back.chainbreak.dev/ws -ELECTRON_WEBPACK_APP_BACKEND_URL = https://back.chainbreak.dev/ \ No newline at end of file +#TYPEORM_LOGGING = false \ No newline at end of file diff --git a/database - Copie/lol-stalker.db b/database - Copie/lol-stalker.db new file mode 100644 index 0000000000000000000000000000000000000000..d7536743d235fcb7c042e5e0e619976a75c32a02 GIT binary patch literal 8192 zcmeIuJqp5548ZZc;wtt6g^aqmcmb<)D~Q?)RD8-q`@!Qa9lVtX)PjX>-Q^z$2}#KC zJ0Eiwtlk=5l|eTWi!riQ6_IF^=GV`@b%=g86_KnDuX%qJi!9z#=j;(c009ILKmY** z5I_I{1Q0-AB# .leftJoinAndSelect("friend.friendNames", "friendNames") .leftJoinAndSelect("friend.notifications", "notifications") .orderBy("rankings.createdAt", "DESC") + .orderBy("notifications.createdAt", "DESC") + .orderBy("friendNames.createdAt", "DESC") .where("friend.puuid = :puuid", { puuid }) .getOne(); diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 2b5bda5..2610e76 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -4,7 +4,7 @@ import { pick } from "@pastable/core"; import fs from "fs/promises"; import electronIsDev from "electron-is-dev"; import { DiscordAuth, editStoreEntry, store } from "../store"; -import { sendToClient } from "../../utils"; +import { sendToClient, wsUrl } from "../../utils"; export const makeSocketClient = async () => { const client = new WebSocketClient(); @@ -63,12 +63,7 @@ export const makeSocketClient = async () => { ) ); - client.connect( - (electronIsDev && false ? "http://localhost:8080/ws" : "https://back.chainbreak.dev/ws") + - "?" + - search.toString(), - "echo-protocol" - ); + client.connect(wsUrl + "?" + search.toString(), "echo-protocol"); return client; }; diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index 67e8da5..c55f2f6 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -1,7 +1,7 @@ import { Notification } from "electron"; import { Friend } from "../entities/Friend"; -import { checkFriendList, compareFriends } from "../features/lcu/lcu"; -import { getRankDifference, sendToClient } from "../utils"; +import { checkFriendList, compareFriends, getAllApexLeague } from "../features/lcu/lcu"; +import { getRankDifference, sendToClient, Tier } from "../utils"; import { getFriendsAndLastRankingFromDb, addRanking } from "../features/routes/friends"; import { addNotification } from "../features/routes/notifications"; import { sendWs } from "../features/ws/discord"; @@ -21,6 +21,7 @@ export const startCheckFriendListJob = async () => { while (true) { const friendListStats = await checkFriendList(); const changes = await compareFriends(store.friends!, friendListStats); + const apexFromLCU = await getAllApexLeague(); if (changes.length) { console.log( `${changes.length} change${changes.length > 1 ? "s" : ""} found in friendList` @@ -33,6 +34,11 @@ export const startCheckFriendListJob = async () => { change.oldFriend as any, change as any ); + + const apex = + apexFromLCU[change.oldFriend.tier as Tier] || + apexFromLCU[change.tier as Tier]; + const payload = { ...notification, fromDivision: change.oldFriend.division, @@ -43,8 +49,11 @@ export const startCheckFriendListJob = async () => { toLeaguePoints: change.leaguePoints, puuid: change.puuid, name: change.name, + apex, }; + sendWs("update", payload); + const friend = new Friend(); friend.puuid = change.puuid; if (store.config?.windowsNotifications && change.windowsNotification) diff --git a/electron/utils.ts b/electron/utils.ts index 2912a76..1b32b40 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -4,6 +4,9 @@ import electronIsDev from "electron-is-dev"; import path from "path"; import { Ranking } from "./entities/Ranking"; +export const baseURL = electronIsDev ? "http://localhost:8080/" : "https://back.chainbreak.dev/"; +export const wsUrl = baseURL + "ws"; + export const sendToClient = (channel: string, ...args: any[]) => console.log(channel)! || BrowserWindow.getAllWindows()?.[0]?.webContents.send(channel, ...args); diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index 8e2ba3d..0000000 --- a/src/api.ts +++ /dev/null @@ -1,4 +0,0 @@ -import axios from "axios"; -import electronIsDev from "electron-is-dev"; -const baseURL = electronIsDev && false ? "http://localhost:8080/" : "https://back.chainbreak.dev/"; -export const api = axios.create({ baseURL }); diff --git a/src/features/FriendDetails/FriendDetails.tsx b/src/features/FriendDetails/FriendDetails.tsx index 473d52f..2a97da9 100644 --- a/src/features/FriendDetails/FriendDetails.tsx +++ b/src/features/FriendDetails/FriendDetails.tsx @@ -72,7 +72,7 @@ export const FriendDetails = () => { setState={setState as (state: string) => void} state={state} /> - + {renderComponentByState[state](friend)}
diff --git a/src/features/FriendDetails/FriendMatches.tsx b/src/features/FriendDetails/FriendMatches.tsx index 22f1e73..ea2a7a7 100644 --- a/src/features/FriendDetails/FriendMatches.tsx +++ b/src/features/FriendDetails/FriendMatches.tsx @@ -32,7 +32,7 @@ export const FriendMatches = ({ puuid }: Pick) => { const games = matchObj?.games?.games; return ( -
+ {games.length ? ( {games.map((game) => ( @@ -42,7 +42,7 @@ export const FriendMatches = ({ puuid }: Pick) => { ) : ( <>No game )} -
+
); }; @@ -58,7 +58,7 @@ export const GameRow = ({ game }: { game: Game }) => { const items = useItemsDataByIds(makeArrayOf(7).map((_, index) => stats["item" + index])); return ( - + {champion ? : }
{ return ( - + {
) : ( - + Recent notifications @@ -54,7 +61,7 @@ export const Notifications = () => { /> )} - + From d957c7a2293ffb706d30d4d5bbfc7ff7464164d4 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Tue, 8 Feb 2022 18:59:08 +0100 Subject: [PATCH 10/35] feat: send apex to ws backend --- electron/index.ts | 3 +++ electron/jobs/updateApex.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 electron/jobs/updateApex.ts diff --git a/electron/index.ts b/electron/index.ts index 0b1212c..65d033c 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -10,6 +10,7 @@ import { connector } from "./features/lcu/lcu"; import { registerInternalRoutes } from "./features/routes/internal"; import { makeSocketClient } from "./features/ws/discord"; import { loadStore } from "./features/store"; +import { startUpdateApex } from "./jobs/updateApex"; const height = 600; const width = 1200; @@ -65,6 +66,8 @@ if (!gotTheLock && !isDev) { startCheckFriendListJob(); startCheckCurrentSummonerRank(); + startUpdateApex(); + app.on("activate", function () { // 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. diff --git a/electron/jobs/updateApex.ts b/electron/jobs/updateApex.ts new file mode 100644 index 0000000..1999a0a --- /dev/null +++ b/electron/jobs/updateApex.ts @@ -0,0 +1,16 @@ +import { getAllApexLeague } from "../features/lcu/lcu"; +import { sendWs } from "../features/ws/discord"; + +export const startUpdateApex = async () => { + try { + const apex = await getAllApexLeague(); + + sendWs("apex", apex); + + console.log("sent apex to ws backed"); + setTimeout(() => startUpdateApex(), 1000 * 60); + } catch (e) { + console.log("couldn't find apex, retrying in 5s"); + setTimeout(() => startUpdateApex(), 5000); + } +}; From 4c5fb00678ff46ccf0a3cee5ba1fe4a2a5ac2440 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Wed, 9 Feb 2022 14:41:02 +0100 Subject: [PATCH 11/35] feat: new url --- electron/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electron/utils.ts b/electron/utils.ts index 1b32b40..be88c5b 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -4,7 +4,9 @@ import electronIsDev from "electron-is-dev"; import path from "path"; import { Ranking } from "./entities/Ranking"; -export const baseURL = electronIsDev ? "http://localhost:8080/" : "https://back.chainbreak.dev/"; +export const baseURL = electronIsDev + ? "http://localhost:8080/" + : "https://stalker.back.chainbreak.dev/"; export const wsUrl = baseURL + "ws"; export const sendToClient = (channel: string, ...args: any[]) => @@ -67,6 +69,7 @@ interface Rank { } const tiers = [ "IRON", + "BRONZE", "SILVER", "GOLD", "PLATINUM", From 99873530cf1803d73afd4eb5ad8ca1e1ab76fb4f Mon Sep 17 00:00:00 2001 From: ledouxm Date: Wed, 9 Feb 2022 14:41:22 +0100 Subject: [PATCH 12/35] feat: remove in lobby from friend activity --- electron/features/routes/friends.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/features/routes/friends.ts b/electron/features/routes/friends.ts index 8503fdb..dc08c5b 100644 --- a/electron/features/routes/friends.ts +++ b/electron/features/routes/friends.ts @@ -106,7 +106,7 @@ export const addOrUpdateFriends = async (friends: FriendDto[]) => { for (const friend of friends) { if ( - friend.lol.gameStatus !== "outOfGame" && + !["outOfGame", "hosting_RANKED_SOLO_5x5"].includes(friend.lol.gameStatus) && friend.lol.gameQueueType === "RANKED_SOLO_5x5" ) { currentInGame.push({ From 677d317cd9cf3a1c8d20955cc5b33cdf3ac1c5b1 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Wed, 9 Feb 2022 14:41:39 +0100 Subject: [PATCH 13/35] feat: focus window on discord callback --- electron/features/ws/discord.ts | 2 ++ electron/index.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 2610e76..53f96f4 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -5,6 +5,7 @@ import fs from "fs/promises"; import electronIsDev from "electron-is-dev"; import { DiscordAuth, editStoreEntry, store } from "../store"; import { sendToClient, wsUrl } from "../../utils"; +import { focusWindow } from "../.."; export const makeSocketClient = async () => { const client = new WebSocketClient(); @@ -78,6 +79,7 @@ const makeCallback: Record void> = { }, auth: async (data: DiscordAuth) => { sendWs("guilds", { accessToken: data.access_token }); + focusWindow(); await editStoreEntry("discordAuth", data); }, me: async (data) => { diff --git a/electron/index.ts b/electron/index.ts index 65d033c..1265926 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -44,6 +44,11 @@ export function makeWindow() { } const gotTheLock = app.requestSingleInstanceLock(); +export const focusWindow = () => { + window?.show(); + window?.focus(); +}; + if (!gotTheLock && !isDev) { app.quit(); } else { From 0f10adb70d470817c3915bb09069b857056ecac3 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Wed, 9 Feb 2022 14:41:48 +0100 Subject: [PATCH 14/35] feat: game status --- src/features/Notifications/InGameFriends.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/features/Notifications/InGameFriends.tsx b/src/features/Notifications/InGameFriends.tsx index c029119..cd53392 100644 --- a/src/features/Notifications/InGameFriends.tsx +++ b/src/features/Notifications/InGameFriends.tsx @@ -113,15 +113,9 @@ const CountDown = ({ timeStamp, ...props }: { timeStamp: number } & BoxProps) =>
); }; -type GameStatus = "hosting_RANKED_SOLO_5x5" | "inQueue" | "inGame" | "championSelect"; -const gameStatusOrder: GameStatus[] = [ - "inGame", - "championSelect", - "inQueue", - "hosting_RANKED_SOLO_5x5", -]; +type GameStatus = "inQueue" | "inGame" | "championSelect"; +const gameStatusOrder: GameStatus[] = ["inGame", "championSelect", "inQueue"]; const gameStatusLabelMap: Record = { - hosting_RANKED_SOLO_5x5: "In Lobby", inQueue: "In queue", inGame: "In game", championSelect: "In champion select", From edf50f670e58829344797ddd79bb2a25965f18e8 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Wed, 9 Feb 2022 14:41:56 +0100 Subject: [PATCH 15/35] feat: send all notification infos to ws --- electron/jobs/currentSummonerRank.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index 1c713c4..b351c44 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -58,11 +58,18 @@ export const startCheckCurrentSummonerRank = async () => { await manager.save(ranking); if (lastRanking) { const diff = getRankDifference(lastRanking, ranking); - sendWs("update", { + const payload = { ...diff, puuid: currentSummonerFromLCU.puuid, name: currentSummonerFromLCU.displayName, - }); + fromTier: lastRanking.tier, + fromDivision: lastRanking.division, + fromLeaguePoints: lastRanking.leaguePoints, + toTier: ranking.tier, + toDivision: ranking.division, + toLeaguePoints: ranking.leaguePoints, + }; + sendWs("update", payload); } } From a0b9fce690dbbd346d09b851a7d3bed9360e64d5 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Thu, 10 Feb 2022 15:49:54 +0100 Subject: [PATCH 16/35] feat: disable empty cache --- config.json | 7 -- discordAuth.json | 7 -- electron/features/autoLaunch.ts | 39 +++++++++ electron/features/routes/internal.ts | 15 +++- electron/features/store.ts | 25 +++++- electron/features/ws/discord.ts | 15 ++-- electron/index.ts | 8 +- electron/tsconfig.json | 9 ++- electron/utils.ts | 7 +- me.json | 14 ---- package.json | 13 ++- public/LOLstalker_128x128.png | Bin 0 -> 5178 bytes public/LOLstalker_16x16.png | Bin 0 -> 591 bytes public/LOLstalker_256x256 copy.png | Bin 0 -> 11360 bytes public/LOLstalker_32x32.png | Bin 0 -> 1180 bytes public/LOLstalker_48x48.png | Bin 0 -> 1769 bytes public/icon.ico | Bin 0 -> 12544 bytes public/icon.png | Bin 0 -> 11360 bytes src/Home.tsx | 2 +- src/components/Navbar.tsx | 3 +- src/features/Options/OptionsPage.tsx | 27 ++++--- vite.config.ts | 2 +- yarn.lock | 114 +++++++-------------------- 23 files changed, 162 insertions(+), 145 deletions(-) delete mode 100644 config.json delete mode 100644 discordAuth.json create mode 100644 electron/features/autoLaunch.ts delete mode 100644 me.json create mode 100644 public/LOLstalker_128x128.png create mode 100644 public/LOLstalker_16x16.png create mode 100644 public/LOLstalker_256x256 copy.png create mode 100644 public/LOLstalker_32x32.png create mode 100644 public/LOLstalker_48x48.png create mode 100644 public/icon.ico create mode 100644 public/icon.png diff --git a/config.json b/config.json deleted file mode 100644 index 746ba35..0000000 --- a/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "windowsNotifications": false, - "dirname": "C:\\Users\\Martin\\dev\\lol-stalking\\main\\features", - "defaultLossMessage": "😂😂😂😂😂😂😂😂😂😂😂😂😂", - "socketId": "iwwfhzfS8ZxY", - "accessToken": "jYMrQPaDOqxfrX4xPSSwgGNPD7iBlE" -} \ No newline at end of file diff --git a/discordAuth.json b/discordAuth.json deleted file mode 100644 index cd562a0..0000000 --- a/discordAuth.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "access_token": "jYMrQPaDOqxfrX4xPSSwgGNPD7iBlE", - "expires_in": 604800, - "refresh_token": "Dwac1x7i5nX5r9uA4MTWYeVdLxFIPX", - "scope": "guilds identify", - "token_type": "Bearer" -} \ No newline at end of file diff --git a/electron/features/autoLaunch.ts b/electron/features/autoLaunch.ts new file mode 100644 index 0000000..eb0939c --- /dev/null +++ b/electron/features/autoLaunch.ts @@ -0,0 +1,39 @@ +import AutoLaunch from "auto-launch"; +import electronIsDev from "electron-is-dev"; +import { editStoreEntry, store } from "./store"; + +export const initAutoLauch = async () => { + const autoLaunch = new AutoLaunch({ name: "LoL Stalker" }); + await editStoreEntry("autoLaunch", autoLaunch); + + if (electronIsDev) return console.log("AutoLaunch doesn't work in development"); + const isEnabled = await autoLaunch.isEnabled(); + + if (store.config.autoLaunch && !isEnabled) autoLaunch.enable(); + else if (isEnabled) autoLaunch.disable(); +}; + +export const enableAutoLaunch = async () => { + console.log("enabling autolaunch..."); + if (electronIsDev) return console.log("AutoLaunch doesn't work in development"); + const autoLaunch = store.autoLaunch; + if (!autoLaunch) return; + + const isEnabled = await autoLaunch.isEnabled(); + + if (isEnabled) return; + autoLaunch.enable(); + console.log("autolaunch enabled"); +}; + +export const disableAutoLauch = async () => { + console.log("disabling autolaunch..."); + const autoLaunch = store.autoLaunch; + if (!autoLaunch) return; + + const isEnabled = await autoLaunch.isEnabled(); + + if (!isEnabled) return; + autoLaunch.disable(); + console.log("autolaunch disabled"); +}; diff --git a/electron/features/routes/internal.ts b/electron/features/routes/internal.ts index 3586210..7b69e8e 100644 --- a/electron/features/routes/internal.ts +++ b/electron/features/routes/internal.ts @@ -12,14 +12,27 @@ import { sendSelectAllFriends, } from "."; import { sendToClient, getDbPath } from "../../utils"; +import { disableAutoLauch, enableAutoLaunch } from "../autoLaunch"; import { getCurrentSummoner, sendConnectorStatus } from "../lcu/lcu"; -import { DiscordUrls, editStoreEntry, sendStore, sendStoreEntry, Store, store } from "../store"; +import { + DiscordUrls, + editStoreEntry, + emptyCache, + sendStore, + sendStoreEntry, + Store, + store, +} from "../store"; import { makeSocketClient, sendWs } from "../ws/discord"; const getMe = async () => sendToClient("me", await getCurrentSummoner()); const setConfig: InternalCallback = async (_, data) => { const newConfig = { ...store.config }; Object.entries(data).forEach(([key, val]) => (newConfig[key] = val)); + if (data["autoLaunch"] !== undefined) { + if (data["autoLaunch"]) await enableAutoLaunch(); + else await disableAutoLauch(); + } await editStoreEntry("config", newConfig); }; diff --git a/electron/features/store.ts b/electron/features/store.ts index 1194b57..2663d3e 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -6,6 +6,7 @@ import path from "path"; import electronIsDev from "electron-is-dev"; import { CurrentSummoner } from "./lcu/types"; import { app } from "electron"; +import AutoLaunch from "auto-launch"; export const initialConfig = { windowsNotifications: true, @@ -53,6 +54,7 @@ export interface Store { discordUrls: null | DiscordUrls; leagueSummoner: null | CurrentSummoner; me: any | null; + autoLaunch: AutoLaunch | null; } interface StoreConfig { @@ -61,8 +63,7 @@ interface StoreConfig { formatter?: (data: any) => any; onLoad?: (data: any) => any; } - -export const store: Store = { +const initialStore: Store = { config: initialConfig, selectedFriends: null, connectorStatus: null, @@ -76,7 +77,11 @@ export const store: Store = { discordUrls: null, leagueSummoner: null, me: null, + autoLaunch: null, }; +const resetStore = () => + Object.entries(initialStore).forEach(([key, value]) => (store[key as keyof Store] = value)); +export const store: Store = { ...initialStore }; const storeConfig: Partial> = { config: { @@ -144,6 +149,7 @@ export const getValue = (entryName: keyof Store) => storeConfig[entryName]?.formatter?.(store[entryName]) || store[entryName]; export const loadStore = async () => { + resetStore(); try { await fs.stat(jsonFolderPath); await fs.readdir(jsonFolderPath); @@ -163,11 +169,26 @@ export const loadStore = async () => { } } catch (e) { console.log("Couldn't load ", entryName + ".json"); + store[entryName] = undefined; console.error(e); } } }; +export const emptyCache = async () => { + const files = await fs.readdir(jsonFolderPath); + for (const file of files) { + try { + await fs.rm(file); + console.log("deleted", file); + } catch (e) { + console.log(e); + } + } + + await loadStore(); +}; + const jsonFolderPath = path.join(app.getPath("userData"), "jsons"); const getJsonPath = (name: string) => path.join(jsonFolderPath, name + ".json"); diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 53f96f4..2cac50b 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -1,10 +1,7 @@ import { client as WebSocketClient, connection } from "websocket"; import DiscordOauth2 from "discord-oauth2"; -import { pick } from "@pastable/core"; -import fs from "fs/promises"; -import electronIsDev from "electron-is-dev"; import { DiscordAuth, editStoreEntry, store } from "../store"; -import { sendToClient, wsUrl } from "../../utils"; +import { wsUrl } from "../../utils"; import { focusWindow } from "../.."; export const makeSocketClient = async () => { const client = new WebSocketClient(); @@ -16,6 +13,8 @@ export const makeSocketClient = async () => { "socketStatus", error.code === "ECONNREFUSED" ? "can't reach server" : "error" ); + console.log("socket closed, retrying in 3s..."); + setTimeout(() => connect(), 3000); }); await editStoreEntry("socketStatus", "connecting"); @@ -35,6 +34,8 @@ export const makeSocketClient = async () => { connection.on("error", async function (error) { console.log("Connection Error: " + error.toString()); await editStoreEntry("socketStatus", "error"); + console.log("socket closed, retrying in 3s..."); + setTimeout(() => connect(), 3000); }); connection.on("close", async function () { @@ -42,6 +43,8 @@ export const makeSocketClient = async () => { clearInterval(interval); console.log("echo-protocol Connection Closed"); await editStoreEntry("socketStatus", "closed"); + console.log("socket closed, retrying in 3s..."); + setTimeout(() => connect(), 3000); }); connection.on("message", function (message) { @@ -64,8 +67,8 @@ export const makeSocketClient = async () => { ) ); - client.connect(wsUrl + "?" + search.toString(), "echo-protocol"); - + const connect = () => client.connect(wsUrl + "?" + search.toString(), "echo-protocol"); + connect(); return client; }; diff --git a/electron/index.ts b/electron/index.ts index 1265926..4db8b11 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -11,6 +11,7 @@ import { registerInternalRoutes } from "./features/routes/internal"; import { makeSocketClient } from "./features/ws/discord"; import { loadStore } from "./features/store"; import { startUpdateApex } from "./jobs/updateApex"; +import { initAutoLauch } from "./features/autoLaunch"; const height = 600; const width = 1200; @@ -26,6 +27,7 @@ export function makeWindow() { show: true, resizable: true, autoHideMenuBar: true, + icon: path.join(__dirname, "../public/icon.ico"), fullscreenable: true, webPreferences: { preload: path.join(__dirname, "preload.js"), @@ -37,9 +39,8 @@ export function makeWindow() { const port = process.env.PORT || 3001; const url = isDev ? `https://localhost:${port}` : path.join(__dirname, "../src/out/index.html"); // window.webContents.openDevTools(); - isDev ? window?.loadURL(url) : window?.loadFile(url); - + console.log(__dirname); return window; } const gotTheLock = app.requestSingleInstanceLock(); @@ -62,8 +63,9 @@ if (!gotTheLock && !isDev) { }); // Create window, load the rest of the app, etc... app.whenReady().then(async () => { - await makeDb(); await loadStore(); + await initAutoLauch(); + await makeDb(); connector.start(); registerInternalRoutes(); await makeSocketClient(); diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 77d7f44..f272e10 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -22,5 +22,12 @@ "outDir": "../main" }, "exclude": ["node_modules"], - "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.prisma", "prismaClient/*"] + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.prisma", + "prismaClient/*", + "../ormconfig.js" + ] } diff --git a/electron/utils.ts b/electron/utils.ts index be88c5b..7bc762f 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -116,5 +116,10 @@ export const getRankDifference = (oldRank: Rank, newRank: Rank) => { export const getDbPath = () => path.join( __dirname, - electronIsDev ? "../database/lol-stalker.db" : "./database/lol-stalker.db" + path.join( + process.env.APPDATA!, + "LoL Stalker", + "database", + electronIsDev ? "lol-stalker.db" : "lol-stalker.dev.db" + ) ); diff --git a/me.json b/me.json deleted file mode 100644 index cb2efcb..0000000 --- a/me.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "730382245352833115", - "username": "Tinmar", - "avatar": "b1788cb649062c8fc751d94578adbd4c", - "discriminator": "3074", - "public_flags": 0, - "flags": 0, - "banner": null, - "banner_color": null, - "accent_color": null, - "locale": "fr", - "mfa_enabled": false, - "premium_type": 2 -} \ No newline at end of file diff --git a/package.json b/package.json index c98e3f2..6de5914 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@emotion/react": "^11", "@emotion/styled": "^11", "@pastable/core": "^0.1.15", + "auto-launch": "^5.0.5", "axios": "^0.24.0", "classnames": "^2.3.1", "debug": "^4.3.3", @@ -64,6 +65,7 @@ "yenv": "^3.0.1" }, "devDependencies": { + "@types/auto-launch": "^5.0.2", "@types/node": "^16.3.3", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", @@ -88,11 +90,18 @@ "build": { "asar": false, "nsis": { - "oneClick": true + "oneClick": true, + "installerIcon": "public/icon.ico", + "installerHeaderIcon": "public/icon.ico" }, "files": [ "main", - "src/out" + "src/out", + "resources/**/*", + { + "from": "public", + "to": "public" + } ], "directories": { "buildResources": "resources" diff --git a/public/LOLstalker_128x128.png b/public/LOLstalker_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..91445ff15cb2f670e027fc6ca06dbb42e781234c GIT binary patch literal 5178 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 literal 0 HcmV?d00001 diff --git a/public/LOLstalker_16x16.png b/public/LOLstalker_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..e904f0ee13ba19569eb1e6c8d6a3f3f537d5a536 GIT binary patch literal 591 zcmV-V0qfls-nl2ZZXDgvcfccY+pj$rQ7H&>-9)PvkAz%Z1;jPhOX1UMa#stVcw z7)VM0_3d^L{UmtQ{_2}dhYU>gNiJB`2C47vyn7rM?@sN5&ES1L#+L!AR}id8#M)-~ zBBBSaJzo!GD34NMGBsKI_bqwHO9AWk!e}cB#M>ZTtRRIqkVG-YnFr46js=+t*=3hqj@hUUen>6> z*sKa=#Q1iJin$l22yL$Ge#5Ju`$ora+6-PUfIKfu5cZ}Jt!YB5LI~4LG=aGKtl`BM zF4COWoS9@L26`1_AW@s$@4njflRw(TB9@-Fs@I9WbHW(H9k1buzuxn$mmgQQ=Q}I# dU&jvt1^~*N^H~@|UJC#K002ovPDHLkV1hJj3ts>L literal 0 HcmV?d00001 diff --git a/public/LOLstalker_256x256 copy.png b/public/LOLstalker_256x256 copy.png new file mode 100644 index 0000000000000000000000000000000000000000..3629a664cd49f3b5896602335106f978e08d3973 GIT binary patch literal 11360 zcmd6NXH-*N&~-wQqEc0)NfE?CQ$#?7P(qiER3UVv_ok3grFRS>B?vr#1?iw71OzEY zKpU|LGXw%TucHk&fk2?t zS15$>4E6Cipvsl{;11HT3^Mg~4GMJ#aDk{g`#QP^>UcZ2xtO>(IEVZ9yF7qE#A$Tk zs%Bx+ey8t~g(tFrT6nrx#Jh@QWCJ%Uc^DNSK$@zm5@{?wF|J=Y|C@yzo? zUyhVE;%c#5ad?jqBb}qq!n}}*pb>heWHCKm^$sFF@#M zqG_W35B%wWHZU>22eTklrEC{Q#Cw3fG!9-_()E3pm*m#Nzx&q8?Bvp`S*8{Cb8Yu7 z+%U5{L>h&|u-|G@TAwYp&91VL_G=al$dgfcg?ZNV2IQS#2sU~nW>Ki>jNR;MsDYyi zCs$8%`&@tZBQD-__$2ETPTK|IrfqKwPsPxJ{rv#SL{Sb{fX}x?~Lsdw*t}UI; zkv^tl@{J!O_S5fP8CrV#km}RltHETa_?!=e3UwtKrWv5y!@w~eH0EZHN+4u=DV5}G zQHC@|!?M78hb0L9ZTu9WrF{cgJN`}P41LLQIZ99)+andcIO8L#duzGT25E8OCAYs4 z4ppJG_gEauy>DuT{MNRHhDnf4@wbSvuT}F93d(Kxj#((PO7Qk@7fiw*sNMB5sQfk3u-W%687d8D|yeu+bkN^!D{1gO2Dk4pOyF1Y-qWCAckFk?#hcxJytbliC~62p6v_%WgeP zo(58%qaJs*jl>53xL5+YaCtMMJ!&T+R-n=5heT@={C;Gzz5te&Mdd11n9=P=MspS# zvp%l$3y>Kx5wk?sk+?6X z<>J^^YYo<9Mh0nbTuoN?H?7lO{f7I=8mImijOq7`7>yuz*s!~znUOgHS89*X@+^#4 zk{h;@1{>dWoI~R+JM&TqLuP}Xr1uJz$hZE%Ν$h|_fw#^MsT5=glqDi#B8A&qHR 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 IYW6Yz4-AZdP5=M^ literal 0 HcmV?d00001 diff --git a/public/LOLstalker_32x32.png b/public/LOLstalker_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..519f886ed5d09440f5d95c8fa68e92f3f0619b3f GIT binary patch literal 1180 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 LNkvXXu0mjfN`Prz literal 0 HcmV?d00001 diff --git a/public/icon.ico b/public/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..96704b3888afe00023cc9978889d4fc4364a740c GIT binary patch literal 12544 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 literal 0 HcmV?d00001 diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3629a664cd49f3b5896602335106f978e08d3973 GIT binary patch literal 11360 zcmd6NXH-*N&~-wQqEc0)NfE?CQ$#?7P(qiER3UVv_ok3grFRS>B?vr#1?iw71OzEY zKpU|LGXw%TucHk&fk2?t zS15$>4E6Cipvsl{;11HT3^Mg~4GMJ#aDk{g`#QP^>UcZ2xtO>(IEVZ9yF7qE#A$Tk zs%Bx+ey8t~g(tFrT6nrx#Jh@QWCJ%Uc^DNSK$@zm5@{?wF|J=Y|C@yzo? zUyhVE;%c#5ad?jqBb}qq!n}}*pb>heWHCKm^$sFF@#M zqG_W35B%wWHZU>22eTklrEC{Q#Cw3fG!9-_()E3pm*m#Nzx&q8?Bvp`S*8{Cb8Yu7 z+%U5{L>h&|u-|G@TAwYp&91VL_G=al$dgfcg?ZNV2IQS#2sU~nW>Ki>jNR;MsDYyi zCs$8%`&@tZBQD-__$2ETPTK|IrfqKwPsPxJ{rv#SL{Sb{fX}x?~Lsdw*t}UI; zkv^tl@{J!O_S5fP8CrV#km}RltHETa_?!=e3UwtKrWv5y!@w~eH0EZHN+4u=DV5}G zQHC@|!?M78hb0L9ZTu9WrF{cgJN`}P41LLQIZ99)+andcIO8L#duzGT25E8OCAYs4 z4ppJG_gEauy>DuT{MNRHhDnf4@wbSvuT}F93d(Kxj#((PO7Qk@7fiw*sNMB5sQfk3u-W%687d8D|yeu+bkN^!D{1gO2Dk4pOyF1Y-qWCAckFk?#hcxJytbliC~62p6v_%WgeP zo(58%qaJs*jl>53xL5+YaCtMMJ!&T+R-n=5heT@={C;Gzz5te&Mdd11n9=P=MspS# zvp%l$3y>Kx5wk?sk+?6X z<>J^^YYo<9Mh0nbTuoN?H?7lO{f7I=8mImijOq7`7>yuz*s!~znUOgHS89*X@+^#4 zk{h;@1{>dWoI~R+JM&TqLuP}Xr1uJz$hZE%Ν$h|_fw#^MsT5=glqDi#B8A&qHR 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 IYW6Yz4-AZdP5=M^ literal 0 HcmV?d00001 diff --git a/src/Home.tsx b/src/Home.tsx index 7c1ff83..43c6f64 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -65,7 +65,7 @@ const AppRoutes = () => { } /> } /> } /> - {process.env.NODE_ENV !== "PRODUCTION" && } />} + {process.env.NODE_ENV === "development" && } />} } /> ); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 05b4681..05ccf2c 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -13,7 +13,6 @@ export const Navbar = (props: StackProps) => { const hasSubMenu = testRegex(location.pathname); const puuid = hasSubMenu && location.pathname.replace("/friend/", ""); - return ( <> { Friendlist Discord Options - {process.env.NODE_ENV !== "PRODUCTION" && Dev tools} + {process.env.NODE_ENV === "development" && Dev tools} {hasSubMenu && (
diff --git a/src/features/Options/OptionsPage.tsx b/src/features/Options/OptionsPage.tsx index 918ae1a..20db307 100644 --- a/src/features/Options/OptionsPage.tsx +++ b/src/features/Options/OptionsPage.tsx @@ -1,7 +1,6 @@ +import { CopyIcon } from "@chakra-ui/icons"; import { - Box, Button, - ButtonProps, Center, CenterProps, Checkbox, @@ -11,18 +10,13 @@ import { Spinner, Stack, } from "@chakra-ui/react"; -// import { shell } from "electron"; -import { useMutation, useQuery } from "react-query"; -import { electronRequest } from "../../utils"; -import { AiFillGithub, AiFillTwitterCircle } from "react-icons/ai"; -import { CopyIcon } from "@chakra-ui/icons"; -import { useForm } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; -import { api } from "../../../electron/preload"; import { useAtomValue } from "jotai/utils"; -import { configAtom, discordUrlsAtom } from "../../components/LCUConnector"; -import { useEffect } from "react"; -import { FaDiscord } from "react-icons/fa"; +import { useForm } from "react-hook-form"; +import { AiFillGithub, AiFillTwitterCircle } from "react-icons/ai"; +// import { shell } from "electron"; +import { useMutation } from "react-query"; +import { configAtom } from "../../components/LCUConnector"; +import { electronMutation, electronRequest } from "../../utils"; export const OptionsPage = () => { const dlDbMutation = useMutation(() => electronRequest("config/dl-db")); const openExternalBrowserMutation = useMutation((url: string) => @@ -39,6 +33,7 @@ export const OptionsPage = () => {
) : ( - - + Recent notifications - + - {nbNewNotifications && nbNewNotifications > 0 && ( + {!!nbNewNotifications && nbNewNotifications > 0 && ( notificationsQuery.refetch()} w="100%" textAlign="center" bgColor="blue.500" py="10px" - borderRadius="10px 10px 0 0" + m="0" fontWeight="medium" cursor="pointer" > @@ -59,7 +58,7 @@ export const Notifications = () => { notificationPages={notificationPages!} fetchNextPage={notificationsQuery.fetchNextPage} /> - +
)} From 3fcf54a58b79bb9582b9236f73c70d99831e79f8 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Thu, 10 Feb 2022 16:27:17 +0100 Subject: [PATCH 18/35] feat: remove all stalked summoners from discord guild --- src/features/Discord/Discord.tsx | 46 +++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx index e466e1f..9fac4a4 100644 --- a/src/features/Discord/Discord.tsx +++ b/src/features/Discord/Discord.tsx @@ -15,7 +15,6 @@ import { Flex, Icon, IconButton, - List, ListItem, Modal, ModalCloseButton, @@ -27,8 +26,7 @@ import { import { useSelection } from "@pastable/core"; import { useAtomValue } from "jotai/utils"; import { useEffect } from "react"; -import { AiOutlineArrowRight } from "react-icons/ai"; -import { BiFolder, BiLogOut, BiPlusCircle, BiRefresh, BiRightArrow } from "react-icons/bi"; +import { BiFolder, BiLogOut, BiPlusCircle, BiRefresh, BiTrash } from "react-icons/bi"; import { FaDiscord } from "react-icons/fa"; import { useQuery } from "react-query"; import { @@ -116,7 +114,7 @@ const BotInfos = (props: BoxProps) => { channel you want the bot to send messages in - Add stalked summoners in the list + Add stalked summoners to the list
@@ -170,19 +168,35 @@ const DiscordGuildList = (props: BoxProps) => { guilds.map((guild) => ( - - - {guild.name} - {guild.channelName}{" "} - - ({guild.summoners.length}) - - - - {guild.nbStalkers} active stalker - {guild.nbStalkers > 1 ? "s" : ""} - + + + + {guild.name} - {guild.channelName}{" "} + + ({guild.summoners.length}) + + + + + {guild.nbStalkers} active stalker + {guild.nbStalkers > 1 ? "s" : ""} + + + { + e.stopPropagation(); + window.Main.sendMessage("discord/remove-friends", { + channelId: guild.channelId, + guildId: guild.guildId, + summoners: guild.summoners.map( + (summoner) => summoner.puuid + ), + }); + }} + /> - {guild.summoners.map((summoner) => ( From c891b7ec4eb00c9d3071e001d2bd815125168cde Mon Sep 17 00:00:00 2001 From: ledouxm Date: Thu, 10 Feb 2022 20:35:20 +0100 Subject: [PATCH 19/35] feat: use ws instead of websockets + icon --- electron/features/store.ts | 8 +- electron/features/ws/discord.ts | 121 ++++++++++++++++--------------- electron/features/ws/wsUtils.ts | 43 +++++++++++ electron/utils.ts | 7 +- package.json | 9 ++- src/features/Discord/Discord.tsx | 69 ++++++++++-------- yarn.lock | 13 +++- 7 files changed, 166 insertions(+), 104 deletions(-) create mode 100644 electron/features/ws/wsUtils.ts diff --git a/electron/features/store.ts b/electron/features/store.ts index 2663d3e..2e9e183 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -1,12 +1,12 @@ import fs from "fs/promises"; import { AxiosInstance } from "axios"; -import { connection } from "websocket"; import { sendToClient } from "../utils"; import path from "path"; import electronIsDev from "electron-is-dev"; import { CurrentSummoner } from "./lcu/types"; import { app } from "electron"; import AutoLaunch from "auto-launch"; +import { WebSocket } from "ws"; export const initialConfig = { windowsNotifications: true, @@ -46,7 +46,7 @@ export interface Store { connectorStatus: null | ConnectorStatus; lcu: null | AxiosInstance; inGameFriends: null | any[]; - backendSocket: null | connection; + backendSocket: null | WebSocket; userGuilds: null | string[]; discordAuth: null | DiscordAuth; friends: null | any[]; @@ -169,7 +169,6 @@ export const loadStore = async () => { } } catch (e) { console.log("Couldn't load ", entryName + ".json"); - store[entryName] = undefined; console.error(e); } } @@ -190,7 +189,8 @@ export const emptyCache = async () => { }; const jsonFolderPath = path.join(app.getPath("userData"), "jsons"); -const getJsonPath = (name: string) => path.join(jsonFolderPath, name + ".json"); +const getJsonPath = (name: string) => + path.join(jsonFolderPath, (electronIsDev ? "dev." : "") + name + ".json"); export const sendStore = () => { const notified = Object.entries(storeConfig) diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 2cac50b..651f65c 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -1,75 +1,78 @@ -import { client as WebSocketClient, connection } from "websocket"; import DiscordOauth2 from "discord-oauth2"; -import { DiscordAuth, editStoreEntry, store } from "../store"; -import { wsUrl } from "../../utils"; +import WebSocket from "ws"; import { focusWindow } from "../.."; +import { wsUrl } from "../../utils"; +import { DiscordAuth, editStoreEntry, store } from "../store"; export const makeSocketClient = async () => { - const client = new WebSocketClient(); + try { + if (store.backendSocket?.readyState === WebSocket.OPEN) { + return console.log("A ws connection alreay exists"); + } - client.on("connectFailed", async function (error: any) { - console.log("Connect Error: " + error.toString()); - console.log(Object.entries(error)); - await editStoreEntry( - "socketStatus", - error.code === "ECONNREFUSED" ? "can't reach server" : "error" + const params = { + ...(store.config?.socketId ? { id: store.config?.socketId } : {}), + ...(store.discordAuth ? store.discordAuth : {}), + }; + const search = new URLSearchParams( + Object.entries(params).reduce( + (acc, [key, value]) => ({ ...acc, ...(!!value ? { [key]: value } : {}) }), + {} + ) ); - console.log("socket closed, retrying in 3s..."); - setTimeout(() => connect(), 3000); - }); - - await editStoreEntry("socketStatus", "connecting"); - client.on("connect", async function (connection) { - console.log("WebSocket Client Connected"); - store.backendSocket = connection; + const socket = new WebSocket(wsUrl + "?" + search.toString(), {}); + await editStoreEntry("socketStatus", "connecting"); + await editStoreEntry("backendSocket", socket); - await editStoreEntry("socketStatus", "connected"); - - const interval = setInterval(() => sendWs("ping"), 5000); - - if (store.discordAuth?.access_token) { - sendWs("guilds", { accessToken: store.discordAuth.access_token }); - } - - connection.on("error", async function (error) { - console.log("Connection Error: " + error.toString()); + socket.onopen = async () => { + console.log("Connection opened"); + await editStoreEntry("socketStatus", "connected"); + }; + socket.onerror = async (error) => { + console.error("WebSocket error"); await editStoreEntry("socketStatus", "error"); - console.log("socket closed, retrying in 3s..."); - setTimeout(() => connect(), 3000); - }); + }; - connection.on("close", async function () { - store.backendSocket = null as any as connection; - clearInterval(interval); - console.log("echo-protocol Connection Closed"); + //@ts-ignore + socket.onmessage = (event) => onMessage({ event }); + socket.onclose = async () => { + console.log("WebSocket close"); await editStoreEntry("socketStatus", "closed"); - console.log("socket closed, retrying in 3s..."); - setTimeout(() => connect(), 3000); - }); + console.log("Retrying in 3s..."); + setTimeout(() => makeSocketClient(), 3000); + }; + } catch (error) { + console.error(error); + } +}; - connection.on("message", function (message) { - if (message.type === "utf8") { - const { event, data } = JSON.parse(message.utf8Data); - console.log("received", event); - makeCallback[event]?.(data); - } - }); - }); +async function onMessage({ event: msgEvent }: { event: MessageEvent }) { + const length = + msgEvent.data instanceof ArrayBuffer ? msgEvent.data.byteLength : msgEvent.data.length; + // Most likely a "pong" response from our "ping" message + if (!length) return; - const params = { - ...(store.config.socketId ? { id: store.config.socketId } : {}), - ...(store.discordAuth ? store.discordAuth : {}), - }; - const search = new URLSearchParams( - Object.entries(params).reduce( - (acc, [key, value]) => ({ ...acc, ...(!!value ? { [key]: value } : {}) }), - {} - ) - ); + const message = await decode(msgEvent.data); + // Invalid message + if (!message) return; - const connect = () => client.connect(wsUrl + "?" + search.toString(), "echo-protocol"); - connect(); - return client; + // console.log(message, typeof message); + const { event, data } = message; + console.log(event, data); + try { + makeCallback[event]?.(data); + } catch (e) { + console.log(e); + } +} +const decoder = new TextDecoder(); +export const decode = async (payload: ArrayBuffer | string): Promise => { + try { + const data = payload instanceof ArrayBuffer ? decoder.decode(payload) : payload; + return JSON.parse(data); + } catch (err) { + return null as any; + } }; export const oauth = new DiscordOauth2(); diff --git a/electron/features/ws/wsUtils.ts b/electron/features/ws/wsUtils.ts new file mode 100644 index 0000000..503e686 --- /dev/null +++ b/electron/features/ws/wsUtils.ts @@ -0,0 +1,43 @@ +import { wsUrl } from "../../utils"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +export const encode = (payload: Payload) => encoder.encode(JSON.stringify(payload)); + +// export const WS = isServer() ? require("ws") : WebSocket; + +export const serialize = (data: Record) => { + let payload: Record = {}; + for (const key in data) { + if (typeof data[key] === "object") { + payload[key] = JSON.stringify(data[key]); + } else { + payload[key] = data[key]; + } + } + + return payload; +}; + +export const getQueryString = (data: Record) => + new URLSearchParams(serialize(data)).toString(); + +export const getWebsocketURL = () => wsUrl; + +// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState +export enum SocketReadyState { + CONNECTING, + OPEN, + CLOSING, + CLOSED, +} + +export enum WsEvent { + Connecting = "_connecting_", + Open = "open", + Close = "close", + Error = "error", + Any = "_msg_", + Reconnected = "_reconnected_", +} diff --git a/electron/utils.ts b/electron/utils.ts index 7bc762f..b0dbc7d 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -4,10 +4,9 @@ import electronIsDev from "electron-is-dev"; import path from "path"; import { Ranking } from "./entities/Ranking"; -export const baseURL = electronIsDev - ? "http://localhost:8080/" - : "https://stalker.back.chainbreak.dev/"; -export const wsUrl = baseURL + "ws"; +const domain = electronIsDev ? "localhost:8080/" : "stalker.back.chainbreak.dev/"; +export const baseURL = (electronIsDev ? "http://" : "https://") + domain; +export const wsUrl = "ws://" + domain + "ws"; export const sendToClient = (channel: string, ...args: any[]) => console.log(channel)! || BrowserWindow.getAllWindows()?.[0]?.webContents.send(channel, ...args); diff --git a/package.json b/package.json index 6de5914..d14c88c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "lol-stalker", "version": "0.0.2", "license": "MIT", - "main": "main/index.js", + "main": "main/electron/index.js", "author": { "name": "Ledoux Martin" }, @@ -60,8 +60,9 @@ "sqlite3": "^5.0.2", "typeorm": "^0.2.41", "websocket": "^1.0.34", - "ws": "^8.4.2", + "ws": "^8.5.0", "ws-client-js": "^1.0.5", + "xstate": "^4.29.0", "yenv": "^3.0.1" }, "devDependencies": { @@ -94,6 +95,10 @@ "installerIcon": "public/icon.ico", "installerHeaderIcon": "public/icon.ico" }, + "win": { + "target": "nsis", + "icon": "public/icon.ico" + }, "files": [ "main", "src/out", diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx index 9fac4a4..b534d03 100644 --- a/src/features/Discord/Discord.tsx +++ b/src/features/Discord/Discord.tsx @@ -26,7 +26,14 @@ import { import { useSelection } from "@pastable/core"; import { useAtomValue } from "jotai/utils"; import { useEffect } from "react"; -import { BiFolder, BiLogOut, BiPlusCircle, BiRefresh, BiTrash } from "react-icons/bi"; +import { + BiFolder, + BiLogOut, + BiMinusCircle, + BiPlusCircle, + BiRefresh, + BiTrash, +} from "react-icons/bi"; import { FaDiscord } from "react-icons/fa"; import { useQuery } from "react-query"; import { @@ -74,7 +81,6 @@ const BotInfos = (props: BoxProps) => { const guilds = useAtomValue(discordGuildsAtom); const me = useAtomValue(meAtom); - if (!guilds) return null; return (
{ )} - The bot is active on {guilds?.length} of your servers + The bot is active on {guilds?.length || 0} of your servers @@ -168,34 +174,18 @@ const DiscordGuildList = (props: BoxProps) => { guilds.map((guild) => ( - - - - {guild.name} - {guild.channelName}{" "} - - ({guild.summoners.length}) - - - - - {guild.nbStalkers} active stalker - {guild.nbStalkers > 1 ? "s" : ""} - - - { - e.stopPropagation(); - window.Main.sendMessage("discord/remove-friends", { - channelId: guild.channelId, - guildId: guild.guildId, - summoners: guild.summoners.map( - (summoner) => summoner.puuid - ), - }); - }} - /> + + + {guild.name} - {guild.channelName}{" "} + + ({guild.summoners.length}) + + + + + {guild.nbStalkers} active stalker + {guild.nbStalkers > 1 ? "s" : ""} + @@ -261,6 +251,23 @@ const AddSummonerButton = ({ Add summoner
+ {summoners?.length !== 0 && ( +
{ + window.Main.sendMessage("discord/remove-friends", { + channelId: channelId, + guildId: guildId, + summoners: summoners.map((summoner) => summoner.puuid), + }); + }} + userSelect="none" + > + + Remove all +
+ )} Date: Thu, 10 Feb 2022 22:39:06 +0100 Subject: [PATCH 20/35] feat: new icon --- public/LOLstalker_128x128.png | Bin 5178 -> 0 bytes public/LOLstalker_16x16.png | Bin 591 -> 0 bytes public/LOLstalker_256x256 copy.png | Bin 11360 -> 0 bytes public/LOLstalker_32x32.png | Bin 1180 -> 0 bytes public/LOLstalker_48x48.png | Bin 1769 -> 0 bytes public/icon.ico | Bin 12544 -> 6858 bytes public/icon.png | Bin 11360 -> 6527 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/LOLstalker_128x128.png delete mode 100644 public/LOLstalker_16x16.png delete mode 100644 public/LOLstalker_256x256 copy.png delete mode 100644 public/LOLstalker_32x32.png delete mode 100644 public/LOLstalker_48x48.png diff --git a/public/LOLstalker_128x128.png b/public/LOLstalker_128x128.png deleted file mode 100644 index 91445ff15cb2f670e027fc6ca06dbb42e781234c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5178 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 diff --git a/public/LOLstalker_16x16.png b/public/LOLstalker_16x16.png deleted file mode 100644 index e904f0ee13ba19569eb1e6c8d6a3f3f537d5a536..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 591 zcmV-V0qfls-nl2ZZXDgvcfccY+pj$rQ7H&>-9)PvkAz%Z1;jPhOX1UMa#stVcw z7)VM0_3d^L{UmtQ{_2}dhYU>gNiJB`2C47vyn7rM?@sN5&ES1L#+L!AR}id8#M)-~ zBBBSaJzo!GD34NMGBsKI_bqwHO9AWk!e}cB#M>ZTtRRIqkVG-YnFr46js=+t*=3hqj@hUUen>6> z*sKa=#Q1iJin$l22yL$Ge#5Ju`$ora+6-PUfIKfu5cZ}Jt!YB5LI~4LG=aGKtl`BM zF4COWoS9@L26`1_AW@s$@4njflRw(TB9@-Fs@I9WbHW(H9k1buzuxn$mmgQQ=Q}I# dU&jvt1^~*N^H~@|UJC#K002ovPDHLkV1hJj3ts>L diff --git a/public/LOLstalker_256x256 copy.png b/public/LOLstalker_256x256 copy.png deleted file mode 100644 index 3629a664cd49f3b5896602335106f978e08d3973..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11360 zcmd6NXH-*N&~-wQqEc0)NfE?CQ$#?7P(qiER3UVv_ok3grFRS>B?vr#1?iw71OzEY zKpU|LGXw%TucHk&fk2?t zS15$>4E6Cipvsl{;11HT3^Mg~4GMJ#aDk{g`#QP^>UcZ2xtO>(IEVZ9yF7qE#A$Tk zs%Bx+ey8t~g(tFrT6nrx#Jh@QWCJ%Uc^DNSK$@zm5@{?wF|J=Y|C@yzo? zUyhVE;%c#5ad?jqBb}qq!n}}*pb>heWHCKm^$sFF@#M zqG_W35B%wWHZU>22eTklrEC{Q#Cw3fG!9-_()E3pm*m#Nzx&q8?Bvp`S*8{Cb8Yu7 z+%U5{L>h&|u-|G@TAwYp&91VL_G=al$dgfcg?ZNV2IQS#2sU~nW>Ki>jNR;MsDYyi zCs$8%`&@tZBQD-__$2ETPTK|IrfqKwPsPxJ{rv#SL{Sb{fX}x?~Lsdw*t}UI; zkv^tl@{J!O_S5fP8CrV#km}RltHETa_?!=e3UwtKrWv5y!@w~eH0EZHN+4u=DV5}G zQHC@|!?M78hb0L9ZTu9WrF{cgJN`}P41LLQIZ99)+andcIO8L#duzGT25E8OCAYs4 z4ppJG_gEauy>DuT{MNRHhDnf4@wbSvuT}F93d(Kxj#((PO7Qk@7fiw*sNMB5sQfk3u-W%687d8D|yeu+bkN^!D{1gO2Dk4pOyF1Y-qWCAckFk?#hcxJytbliC~62p6v_%WgeP zo(58%qaJs*jl>53xL5+YaCtMMJ!&T+R-n=5heT@={C;Gzz5te&Mdd11n9=P=MspS# zvp%l$3y>Kx5wk?sk+?6X z<>J^^YYo<9Mh0nbTuoN?H?7lO{f7I=8mImijOq7`7>yuz*s!~znUOgHS89*X@+^#4 zk{h;@1{>dWoI~R+JM&TqLuP}Xr1uJz$hZE%Ν$h|_fw#^MsT5=glqDi#B8A&qHR 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 IYW6Yz4-AZdP5=M^ diff --git a/public/LOLstalker_32x32.png b/public/LOLstalker_32x32.png deleted file mode 100644 index 519f886ed5d09440f5d95c8fa68e92f3f0619b3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1180 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 LNkvXXu0mjfN`Prz diff --git a/public/icon.ico b/public/icon.ico index 96704b3888afe00023cc9978889d4fc4364a740c..e0b68eb694423a175757c7648c93c3aa21f80f0e 100644 GIT binary patch literal 6858 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 C0fKe_ literal 12544 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 diff --git a/public/icon.png b/public/icon.png index 3629a664cd49f3b5896602335106f978e08d3973..ed226d1b39b1b8f2b6c617e85405b4f86ebc3eda 100644 GIT binary patch literal 6527 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Ν$h|_fw#^MsT5=glqDi#B8A&qHR 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 IYW6Yz4-AZdP5=M^ From 08ba6b7545699a61756bb11f2244b00ca1db03c4 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Thu, 10 Feb 2022 22:39:14 +0100 Subject: [PATCH 21/35] fix: build --- electron/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electron/index.ts b/electron/index.ts index 4db8b11..a6926fd 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -37,7 +37,9 @@ export function makeWindow() { }, }); const port = process.env.PORT || 3001; - const url = isDev ? `https://localhost:${port}` : path.join(__dirname, "../src/out/index.html"); + const url = isDev + ? `https://localhost:${port}` + : path.join(__dirname, "../../src/out/index.html"); // window.webContents.openDevTools(); isDev ? window?.loadURL(url) : window?.loadFile(url); console.log(__dirname); From 1d189fa3d228be7b9c353c5d9422d8cdc3816584 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Thu, 10 Feb 2022 22:56:17 +0100 Subject: [PATCH 22/35] refactor: split Discord.tsx into multiple files --- database - Copie/lol-stalker.db | Bin 8192 -> 0 bytes discordUrls.json | 4 - src/features/Discord/AddSummonerButton.tsx | 55 ++++++++ src/features/Discord/BotInfos.tsx | 70 ++++++++++ src/features/Discord/BotInvitation.tsx | 23 +++ src/features/Discord/DiscordGuildList.tsx | 86 ++++++++++++ src/features/Discord/DiscordLoginButton.tsx | 26 ++++ src/features/Discord/SummonerPanel.tsx | 31 +++++ src/features/Discord/SummonerSelection.tsx | 147 ++++++++++++++++++++ src/features/Discord/discordUtils.tsx | 6 + 10 files changed, 444 insertions(+), 4 deletions(-) delete mode 100644 database - Copie/lol-stalker.db delete mode 100644 discordUrls.json create mode 100644 src/features/Discord/AddSummonerButton.tsx create mode 100644 src/features/Discord/BotInfos.tsx create mode 100644 src/features/Discord/BotInvitation.tsx create mode 100644 src/features/Discord/DiscordGuildList.tsx create mode 100644 src/features/Discord/DiscordLoginButton.tsx create mode 100644 src/features/Discord/SummonerPanel.tsx create mode 100644 src/features/Discord/SummonerSelection.tsx create mode 100644 src/features/Discord/discordUtils.tsx diff --git a/database - Copie/lol-stalker.db b/database - Copie/lol-stalker.db deleted file mode 100644 index d7536743d235fcb7c042e5e0e619976a75c32a02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeIuJqp5548ZZc;wtt6g^aqmcmb<)D~Q?)RD8-q`@!Qa9lVtX)PjX>-Q^z$2}#KC zJ0Eiwtlk=5l|eTWi!riQ6_IF^=GV`@b%=g86_KnDuX%qJi!9z#=j;(c009ILKmY** z5I_I{1Q0-AB# { + const disclosure = useDisclosure(); + + return ( + <> +
+
disclosure.onOpen()} userSelect="none"> + + Add summoner +
+ {summoners?.length !== 0 && ( +
{ + window.Main.sendMessage("discord/remove-friends", { + channelId: channelId, + guildId: guildId, + summoners: summoners.map((summoner) => summoner.puuid), + }); + }} + userSelect="none" + > + + Remove all +
+ )} +
+ + + + + ); +}; diff --git a/src/features/Discord/BotInfos.tsx b/src/features/Discord/BotInfos.tsx new file mode 100644 index 0000000..6a421c9 --- /dev/null +++ b/src/features/Discord/BotInfos.tsx @@ -0,0 +1,70 @@ +import { + Box, + BoxProps, + Center, + chakra, + Divider, + Flex, + Icon, + IconButton, + ListItem, + UnorderedList, +} from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { BiLogOut } from "react-icons/bi"; +import { FaDiscord } from "react-icons/fa"; +import { discordGuildsAtom, meAtom } from "../../components/LCUConnector"; +import { electronMutation } from "../../utils"; +import { BotInvitation } from "./BotInvitation"; + +export const BotInfos = (props: BoxProps) => { + const guilds = useAtomValue(discordGuildsAtom); + const me = useAtomValue(meAtom); + + return ( +
+ + {me && ( + + + + Connected as + + {me.username} #{me.discriminator} + + + electronMutation("store/set", { discordAuth: null })} + icon={} + aria-label="Logout" + /> + + )} + + + The bot is active on {guilds?.length || 0} of your servers + + + + Invite the bot to your Discord server + + Send !stalker init in the + channel you want the bot to send messages in + + + Add stalked summoners to the list + + + + +
+ ); +}; diff --git a/src/features/Discord/BotInvitation.tsx b/src/features/Discord/BotInvitation.tsx new file mode 100644 index 0000000..6bbd1d6 --- /dev/null +++ b/src/features/Discord/BotInvitation.tsx @@ -0,0 +1,23 @@ +import { ExternalLinkIcon } from "@chakra-ui/icons"; +import { Box, Button, Center, CenterProps } from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { discordUrlsAtom } from "../../components/LCUConnector"; +import { electronRequest } from "../../utils"; + +export const BotInvitation = (props: CenterProps) => { + const discordUrls = useAtomValue(discordUrlsAtom); + + if (!discordUrls) return ; + + return ( +
+ + +
+ ); +}; diff --git a/src/features/Discord/DiscordGuildList.tsx b/src/features/Discord/DiscordGuildList.tsx new file mode 100644 index 0000000..1f2b278 --- /dev/null +++ b/src/features/Discord/DiscordGuildList.tsx @@ -0,0 +1,86 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + BoxProps, + chakra, + Flex, + IconButton, + Stack, +} from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { useEffect } from "react"; +import { BiRefresh } from "react-icons/bi"; +import { discordGuildsAtom } from "../../components/LCUConnector"; +import { refreshGuilds } from "./Discord"; +import { SummonerPanel } from "./SummonerPanel"; +import { AddSummonerButton } from "./AddSummonerButton"; + +export const DiscordGuildList = (props: BoxProps) => { + const guilds = useAtomValue(discordGuildsAtom); + + useEffect(() => { + if (!guilds) refreshGuilds(); + }, [guilds]); + + return ( + + + + Stalked summoners + + refreshGuilds()} + icon={} + aria-label="Refresh guilds" + /> + + + {!guilds?.length ? ( + No guild + ) : ( + guilds.map((guild) => ( + + + + + {guild.name} - {guild.channelName}{" "} + + ({guild.summoners.length}) + + + + + {guild.nbStalkers} active stalker + {guild.nbStalkers > 1 ? "s" : ""} + + + + + {guild.summoners.map((summoner) => ( + + ))} + + + + )) + )} + + + + ); +}; diff --git a/src/features/Discord/DiscordLoginButton.tsx b/src/features/Discord/DiscordLoginButton.tsx new file mode 100644 index 0000000..e17f4a2 --- /dev/null +++ b/src/features/Discord/DiscordLoginButton.tsx @@ -0,0 +1,26 @@ +import { Button, Center, CenterProps, Icon } from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { useEffect } from "react"; +import { FaDiscord } from "react-icons/fa"; +import { discordUrlsAtom } from "../../components/LCUConnector"; +import { electronRequest } from "../../utils"; + +export const DiscordLoginButton = (props: CenterProps) => { + const discordUrls = useAtomValue(discordUrlsAtom); + + useEffect(() => { + if (!discordUrls) electronRequest("config/discord-urls"); + }, [discordUrls]); + + return ( +
+ + +
+ ); +}; diff --git a/src/features/Discord/SummonerPanel.tsx b/src/features/Discord/SummonerPanel.tsx new file mode 100644 index 0000000..c5fb6cb --- /dev/null +++ b/src/features/Discord/SummonerPanel.tsx @@ -0,0 +1,31 @@ +import { CloseIcon } from "@chakra-ui/icons"; +import { Box, Flex } from "@chakra-ui/react"; +import { DiscordGuild } from "../../components/LCUConnector"; +import { electronMutation } from "../../utils"; + +export const SummonerPanel = ({ + summoner, + guildId, + channelId, +}: { + summoner: DiscordGuild["summoners"][0]; + guildId: string; + channelId: string; +}) => { + return ( + + {summoner.name} + + electronMutation("discord/remove-friends", { + guildId, + channelId, + summoners: [summoner.puuid], + }) + } + /> + + ); +}; diff --git a/src/features/Discord/SummonerSelection.tsx b/src/features/Discord/SummonerSelection.tsx new file mode 100644 index 0000000..73b13ae --- /dev/null +++ b/src/features/Discord/SummonerSelection.tsx @@ -0,0 +1,147 @@ +import { + Accordion, + AccordionButton, + AccordionItem, + AccordionPanel, + Box, + Button, + Center, + Flex, + Icon, + ModalCloseButton, + ModalContent, + Stack, +} from "@chakra-ui/react"; +import { useSelection } from "@pastable/core"; +import { BiFolder } from "react-icons/bi"; +import { useQuery } from "react-query"; +import { DiscordGuild } from "../../components/LCUConnector"; +import { electronRequest } from "../../utils"; +import { useFriendList } from "../FriendList/useFriendList"; +import { getBgColor } from "./discordUtils"; + +type SummonerSelection = Omit; +export const AddSummonerModal = ({ + summoners, + guildName, + guildId, + channelId, + onClose, +}: { + summoners: DiscordGuild["summoners"]; + guildName: string; + guildId: string; + channelId: string; + onClose: () => void; +}) => { + const { friendGroups } = useFriendList(); + const [selection, api] = useSelection({ + getId: (item) => item.puuid, + }); + + const meQuery = useQuery("me", () => electronRequest("me")); + + if (!friendGroups?.length) + return ( +
+ No friend. You can try refreshing the page (CTRL-R) +
+ ); + + const summonersIds = summoners.map((summoner) => summoner.puuid); + const selectionIds = selection.map((selected) => selected.puuid); + + const onClick = () => { + const { toAdd, toRemove } = selection.reduce( + (acc, current) => + summoners.find((summoner) => summoner.puuid === current.puuid) + ? { ...acc, toRemove: [...acc.toAdd, current] } + : { ...acc, toAdd: [...acc.toAdd, current] }, + { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } + ); + if (toAdd.length) { + window.Main.sendMessage("discord/add-friends", { + channelId, + guildId, + summoners: toAdd, + }); + } + if (toRemove.length) { + window.Main.sendMessage("discord/remove-friends", { + channelId, + guildId, + summoners: toRemove.map((summoner) => summoner.puuid), + }); + } + onClose(); + }; + + return ( + + + {selection.length !== 0 && ( +
+ +
+ )} + + Add or remove summoners to {guildName} stalking list + + {meQuery.isSuccess && ( + + api.toggle({ + name: meQuery.data.displayName, + puuid: meQuery.data.puuid, + id: 0, + }) + } + > + Me ({meQuery.data.displayName}) + + )} + + {friendGroups.map((group) => ( + + + + + {group.groupName} + + + + + {group.friends.map((friend) => ( + + api.toggle(friend)} + > + {friend.name} + + + ))} + + + + ))} + +
+ ); +}; diff --git a/src/features/Discord/discordUtils.tsx b/src/features/Discord/discordUtils.tsx new file mode 100644 index 0000000..d83410a --- /dev/null +++ b/src/features/Discord/discordUtils.tsx @@ -0,0 +1,6 @@ +export const getBgColor = (initial: string[], selection: string[], puuid: string) => { + if (initial.includes(puuid) && selection.includes(puuid)) return "red.400"; + if (!initial.includes(puuid) && !selection.includes(puuid)) return "initial"; + if (initial.includes(puuid) && !selection.includes(puuid)) return "blue.400"; + if (!initial.includes(puuid) && selection.includes(puuid)) return "green.400"; +}; From 92020e63f5c24d5f79f27015a7de00aeb742e8ae Mon Sep 17 00:00:00 2001 From: ledouxm Date: Fri, 11 Feb 2022 12:30:04 +0100 Subject: [PATCH 23/35] feat: server restriction + current summoner elo graph --- electron/features/ws/discord.ts | 3 +- electron/index.ts | 6 +- src/Home.tsx | 2 + src/components/LCUConnector.tsx | 5 + src/components/Navbar.tsx | 1 + src/components/SocketStatus.tsx | 1 - src/components/toasts.ts | 57 +++ .../CurrentSummoner/CurrentSummoner.tsx | 36 ++ src/features/Discord/AddSummonerButton.tsx | 9 +- src/features/Discord/Discord.tsx | 434 +----------------- src/features/Discord/DiscordGuildList.tsx | 17 +- src/features/Discord/SummonerSelection.tsx | 37 +- src/features/FriendDetails/FriendDetails.tsx | 2 +- .../FriendDetails/FriendRankingGraph.tsx | 2 +- src/features/FriendDetails/Profile.tsx | 1 - 15 files changed, 157 insertions(+), 456 deletions(-) create mode 100644 src/components/toasts.ts create mode 100644 src/features/CurrentSummoner/CurrentSummoner.tsx diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 651f65c..bde5c10 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -1,7 +1,7 @@ import DiscordOauth2 from "discord-oauth2"; import WebSocket from "ws"; import { focusWindow } from "../.."; -import { wsUrl } from "../../utils"; +import { sendToClient, wsUrl } from "../../utils"; import { DiscordAuth, editStoreEntry, store } from "../store"; export const makeSocketClient = async () => { try { @@ -97,6 +97,7 @@ const makeCallback: Record void> = { sendWs("guilds", { accessToken: store.discordAuth?.access_token }), discordUrls: async (data) => editStoreEntry("discordUrls", data), invalidToken: () => editStoreEntry("discordAuth", null), + error: async (data) => sendToClient("error", data), }; export const sendWs = (event: string, data?: any) => diff --git a/electron/index.ts b/electron/index.ts index a6926fd..8d71c60 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -56,7 +56,7 @@ if (!gotTheLock && !isDev) { app.quit(); } else { if (!isDev) - app.on("second-instance", (event, commandLine, workingDirectory) => { + app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. if (window) { if (window.isMinimized()) window.restore(); @@ -86,13 +86,13 @@ if (!gotTheLock && !isDev) { app.quit(); process.exit(0); }); - app.on("open-url", (event, url) => { + app.on("open-url", (_, url) => { dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); }); }); // Handle the protocol. In this case, we choose to show an Error Box. - app.on("open-url", (event, url) => { + app.on("open-url", (_, url) => { dialog.showErrorBox("Welcome Back", `You arrived from: ${url}`); }); diff --git a/src/Home.tsx b/src/Home.tsx index 43c6f64..d17ed33 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -11,6 +11,7 @@ import { DevTools } from "./features/DevTools/DevTools"; import { Discord } from "./features/Discord/Discord"; import { lcuStatusAtom, Store } from "./components/LCUConnector"; import { SocketStatus } from "./components/SocketStatus"; +import { CurrentSummoner } from "./features/CurrentSummoner/CurrentSummoner"; export const Home = () => { const lcuStatus = useAtomValue(lcuStatusAtom); @@ -65,6 +66,7 @@ const AppRoutes = () => { } /> } /> } /> + } /> {process.env.NODE_ENV === "development" && } />} } /> diff --git a/src/components/LCUConnector.tsx b/src/components/LCUConnector.tsx index 5a08a2a..ac37446 100644 --- a/src/components/LCUConnector.tsx +++ b/src/components/LCUConnector.tsx @@ -10,6 +10,7 @@ import { useSummonerSpellsList } from "../features/DataDragon/useSummonerSpellsL import { friendsAtom } from "../features/FriendList/useFriendList"; import { AuthData, CurrentSummoner, FriendDto, FriendLastRankDto } from "../types"; import { electronRequest, sendMessage } from "../utils"; +import { errorToast } from "./toasts"; export interface DiscordGuild { channelId: string; @@ -18,6 +19,7 @@ export interface DiscordGuild { channelName: string; nbStalkers: number; summoners: { id: number; puuid: string; channelId: string; name: string }[]; + isRestricted: boolean; } export interface ConnectorStatus { address: string; @@ -99,6 +101,9 @@ export const LCUConnector = () => { setStore((store) => (store ? { ...store, ...data } : null)) ); window.Main.on("friendList/lastRank", (data: FriendLastRankDto[]) => setFriends([...data])); + window.Main.on("error", (data: string) => + errorToast({ title: "An error has occured", description: data }) + ); sendMessage("friendList/lastRank"); return () => diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 05ccf2c..e240f04 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -28,6 +28,7 @@ export const Navbar = (props: StackProps) => { > Notifications Friendlist + My graph Discord Options {process.env.NODE_ENV === "development" && Dev tools} diff --git a/src/components/SocketStatus.tsx b/src/components/SocketStatus.tsx index 2018f17..9f26461 100644 --- a/src/components/SocketStatus.tsx +++ b/src/components/SocketStatus.tsx @@ -4,6 +4,5 @@ import { socketStatusAtom } from "./LCUConnector"; export const SocketStatus = (props: BoxProps) => { const socketStatus = useAtomValue(socketStatusAtom); - console.log(socketStatus); return {socketStatus}; }; diff --git a/src/components/toasts.ts b/src/components/toasts.ts new file mode 100644 index 0000000..b6072dd --- /dev/null +++ b/src/components/toasts.ts @@ -0,0 +1,57 @@ +import { ToastId, UseToastOptions, createStandaloneToast } from "@chakra-ui/react"; +import { getRandomString, isDev, parseStringAsBoolean } from "@pastable/core"; +import { AxiosError } from "axios"; +import theme from "../theme"; + +// Toasts +const toast = createStandaloneToast({ theme }); +const baseToastConfig = { duration: 3000, isClosable: true, unique: true }; + +type ToastStatus = Exclude | "default"; +export const toastConfigs: Record = { + default: { ...baseToastConfig }, + success: { ...baseToastConfig, status: "success" }, + error: { ...baseToastConfig, status: "error" }, + info: { ...baseToastConfig, status: "info" }, + warning: { ...baseToastConfig, status: "warning" }, +}; + +const toastMap = new Map(); +export type ToastOptions = UseToastOptions & UniqueToastOptions; + +export const makeToast = (options: ToastOptions) => { + if (options.uniqueId) { + options.id = getRandomString(10); + const prevToast = toastMap.get(options.uniqueId); + prevToast && toast.close(prevToast.id!); + toastMap.set(options.uniqueId, options); + } else if (options.unique) { + toast.closeAll(); + } + + return toast(options); +}; + +export const defaultToast = (options: ToastOptions) => + makeToast({ ...toastConfigs.default, ...options }); +export const successToast = (options: ToastOptions) => + makeToast({ ...toastConfigs.success, unique: false, ...options }); +export const errorToast = (options?: ToastOptions) => + makeToast({ title: "Une erreur est survenue", ...toastConfigs.error, ...options }); +export const infoToast = (options: ToastOptions) => makeToast({ ...toastConfigs.info, ...options }); +export const warningToast = (options: ToastOptions) => + makeToast({ ...toastConfigs.warning, ...options }); + +// Errors +export const onError = (description: string) => errorToast({ description }); +export const onAxiosError = (err: AxiosError) => { + if (isDev()) console.error(err); + onError(err.response?.data?.error) as any; +}; + +interface UniqueToastOptions { + /** When provided, will close previous toasts with the same id */ + uniqueId?: ToastId; + /** When true, will close all other toasts */ + unique?: boolean; +} diff --git a/src/features/CurrentSummoner/CurrentSummoner.tsx b/src/features/CurrentSummoner/CurrentSummoner.tsx new file mode 100644 index 0000000..32b58b5 --- /dev/null +++ b/src/features/CurrentSummoner/CurrentSummoner.tsx @@ -0,0 +1,36 @@ +import { Box, chakra, Flex, Spinner, Stack } from "@chakra-ui/react"; +import { useAtomValue } from "jotai/utils"; +import { useQuery } from "react-query"; +import { leagueSummonerAtom } from "../../components/LCUConnector"; +import { ProfileIcon } from "../DataDragon/Profileicon"; +import { getFriendRanks } from "../FriendDetails/FriendDetails"; +import { FriendRankingGraph } from "../FriendDetails/FriendRankingGraph"; + +export const CurrentSummoner = () => { + const currentSummoner = useAtomValue(leagueSummonerAtom); + const friendQuery = useQuery( + ["friend", currentSummoner!.puuid], + () => getFriendRanks(currentSummoner!.puuid!), + { enabled: !!currentSummoner } + ); + + if (friendQuery.isLoading) return ; + if (!friendQuery.data) return null; + + return ( + + {currentSummoner && ( + + + + Connected as + {currentSummoner?.displayName} + + + )} + + + + + ); +}; diff --git a/src/features/Discord/AddSummonerButton.tsx b/src/features/Discord/AddSummonerButton.tsx index 8b28c37..90f8058 100644 --- a/src/features/Discord/AddSummonerButton.tsx +++ b/src/features/Discord/AddSummonerButton.tsx @@ -8,12 +8,8 @@ export const AddSummonerButton = ({ channelId, summoners, name, -}: { - name: string; - guildId: string; - channelId: string; - summoners: DiscordGuild["summoners"]; -}) => { + isRestricted, +}: DiscordGuild) => { const disclosure = useDisclosure(); return ( @@ -48,6 +44,7 @@ export const AddSummonerButton = ({ guildName={name} guildId={guildId} channelId={channelId} + isRestricted={isRestricted} />
diff --git a/src/features/Discord/Discord.tsx b/src/features/Discord/Discord.tsx index b534d03..b894bd3 100644 --- a/src/features/Discord/Discord.tsx +++ b/src/features/Discord/Discord.tsx @@ -1,53 +1,13 @@ -import { CloseIcon, ExternalLinkIcon } from "@chakra-ui/icons"; -import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Box, - BoxProps, - Button, - Center, - CenterProps, - chakra, - Divider, - Flex, - Icon, - IconButton, - ListItem, - Modal, - ModalCloseButton, - ModalContent, - Stack, - UnorderedList, - useDisclosure, -} from "@chakra-ui/react"; -import { useSelection } from "@pastable/core"; +import { Box, Center, Divider, Flex, Stack } from "@chakra-ui/react"; import { useAtomValue } from "jotai/utils"; -import { useEffect } from "react"; -import { - BiFolder, - BiLogOut, - BiMinusCircle, - BiPlusCircle, - BiRefresh, - BiTrash, -} from "react-icons/bi"; -import { FaDiscord } from "react-icons/fa"; -import { useQuery } from "react-query"; -import { - discordAuthAtom, - DiscordGuild, - discordGuildsAtom, - discordUrlsAtom, - meAtom, - socketStatusAtom, -} from "../../components/LCUConnector"; -import { electronMutation, electronRequest } from "../../utils"; -import { useFriendList } from "../FriendList/useFriendList"; - -const refreshGuilds = () => electronMutation("discord/guilds"); +import { BiTrash } from "react-icons/bi"; +import { discordAuthAtom, socketStatusAtom } from "../../components/LCUConnector"; +import { electronMutation } from "../../utils"; +import { BotInfos } from "./BotInfos"; +import { DiscordGuildList } from "./DiscordGuildList"; +import { DiscordLoginButton } from "./DiscordLoginButton"; + +export const refreshGuilds = () => electronMutation("discord/guilds"); export const Discord = () => { const discordAuth = useAtomValue(discordAuthAtom); const socketStatus = useAtomValue(socketStatusAtom); @@ -55,7 +15,7 @@ export const Discord = () => { if (socketStatus !== "connected") { return (
- {socketStatus} + Not connected to WS backend, try again later
); } @@ -76,377 +36,3 @@ export const Discord = () => { ); }; - -const BotInfos = (props: BoxProps) => { - const guilds = useAtomValue(discordGuildsAtom); - const me = useAtomValue(meAtom); - - return ( -
- - {me && ( - - - - Connected as - - {me.username} #{me.discriminator} - - - electronMutation("store/set", { discordAuth: null })} - icon={} - aria-label="Logout" - /> - - )} - - - The bot is active on {guilds?.length || 0} of your servers - - - - Invite the bot to your Discord server - - Send !stalker init in the - channel you want the bot to send messages in - - - Add stalked summoners to the list - - - - -
- ); -}; - -const BotInvitation = (props: CenterProps) => { - const discordUrls = useAtomValue(discordUrlsAtom); - - if (!discordUrls) return ; - - return ( -
- - -
- ); -}; - -const DiscordGuildList = (props: BoxProps) => { - const guilds = useAtomValue(discordGuildsAtom); - - useEffect(() => { - if (!guilds) refreshGuilds(); - }, [guilds]); - - return ( - - - - Stalked summoners - - refreshGuilds()} - icon={} - aria-label="Refresh guilds" - /> - - - {!guilds?.length ? ( - No guild - ) : ( - guilds.map((guild) => ( - - - - - {guild.name} - {guild.channelName}{" "} - - ({guild.summoners.length}) - - - - - {guild.nbStalkers} active stalker - {guild.nbStalkers > 1 ? "s" : ""} - - - - - {guild.summoners.map((summoner) => ( - - ))} - - - - )) - )} - - {} - - ); -}; -export const DiscordLoginButton = (props: CenterProps) => { - const discordUrls = useAtomValue(discordUrlsAtom); - - useEffect(() => { - if (!discordUrls) electronRequest("config/discord-urls"); - }, [discordUrls]); - - return ( -
- - -
- ); -}; -const AddSummonerButton = ({ - guildId, - channelId, - summoners, - name, -}: { - name: string; - guildId: string; - channelId: string; - summoners: DiscordGuild["summoners"]; -}) => { - const disclosure = useDisclosure(); - - return ( - <> -
-
disclosure.onOpen()} userSelect="none"> - - Add summoner -
- {summoners?.length !== 0 && ( -
{ - window.Main.sendMessage("discord/remove-friends", { - channelId: channelId, - guildId: guildId, - summoners: summoners.map((summoner) => summoner.puuid), - }); - }} - userSelect="none" - > - - Remove all -
- )} -
- - - - - ); -}; - -type SummonerSelection = Omit; -const AddSummonerModal = ({ - summoners, - guildName, - guildId, - channelId, - onClose, -}: { - summoners: DiscordGuild["summoners"]; - guildName: string; - guildId: string; - channelId: string; - onClose: () => void; -}) => { - const { friendGroups } = useFriendList(); - const [selection, api] = useSelection({ - getId: (item) => item.puuid, - }); - - const meQuery = useQuery("me", () => electronRequest("me")); - - if (!friendGroups?.length) - return ( -
- No friend. You can try refreshing the page (CTRL-R) -
- ); - - const summonersIds = summoners.map((summoner) => summoner.puuid); - const selectionIds = selection.map((selected) => selected.puuid); - - const onClick = () => { - const { toAdd, toRemove } = selection.reduce( - (acc, current) => - summoners.find((summoner) => summoner.puuid === current.puuid) - ? { ...acc, toRemove: [...acc.toAdd, current] } - : { ...acc, toAdd: [...acc.toAdd, current] }, - { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } - ); - if (toAdd.length) { - // console.log("add", toAdd); - window.Main.sendMessage("discord/add-friends", { - channelId, - guildId, - summoners: toAdd, - }); - } - if (toRemove.length) { - // console.log("remove", toRemove); - - window.Main.sendMessage("discord/remove-friends", { - channelId, - guildId, - summoners: toRemove.map((summoner) => summoner.puuid), - }); - } - onClose(); - }; - - return ( - - - {selection.length !== 0 && ( -
- -
- )} - - Add or remove summoners to {guildName} stalking list - - {meQuery.isSuccess && ( - - api.toggle({ - name: meQuery.data.displayName, - puuid: meQuery.data.puuid, - id: 0, - }) - } - > - Me ({meQuery.data.displayName}) - - )} - - {friendGroups.map((group) => ( - - - - - {group.groupName} - - - - - {group.friends.map((friend) => ( - - api.toggle(friend)} - > - {friend.name} - - - ))} - - - - ))} - - {/*
{})}> -
- - setIsEdit((edit) => !edit)} /> -
-
*/} -
- ); -}; - -const getBgColor = (initial: string[], selection: string[], puuid: string) => { - if (initial.includes(puuid) && selection.includes(puuid)) return "red.400"; - if (!initial.includes(puuid) && !selection.includes(puuid)) return "initial"; - if (initial.includes(puuid) && !selection.includes(puuid)) return "blue.400"; - if (!initial.includes(puuid) && selection.includes(puuid)) return "green.400"; -}; - -const SummonerPanel = ({ - summoner, - guildId, - channelId, -}: { - summoner: DiscordGuild["summoners"][0]; - guildId: string; - channelId: string; -}) => { - return ( - - {summoner.name} - - electronMutation("discord/remove-friends", { - guildId, - channelId, - summoners: [summoner.puuid], - }) - } - /> - - ); -}; diff --git a/src/features/Discord/DiscordGuildList.tsx b/src/features/Discord/DiscordGuildList.tsx index 1f2b278..53d925c 100644 --- a/src/features/Discord/DiscordGuildList.tsx +++ b/src/features/Discord/DiscordGuildList.tsx @@ -18,6 +18,7 @@ import { discordGuildsAtom } from "../../components/LCUConnector"; import { refreshGuilds } from "./Discord"; import { SummonerPanel } from "./SummonerPanel"; import { AddSummonerButton } from "./AddSummonerButton"; +import { LockIcon } from "@chakra-ui/icons"; export const DiscordGuildList = (props: BoxProps) => { const guilds = useAtomValue(discordGuildsAtom); @@ -26,6 +27,8 @@ export const DiscordGuildList = (props: BoxProps) => { if (!guilds) refreshGuilds(); }, [guilds]); + console.log(guilds); + return ( @@ -50,7 +53,8 @@ export const DiscordGuildList = (props: BoxProps) => { {guild.name} - {guild.channelName}{" "} - ({guild.summoners.length}) + ({guild.summoners.length}/ + {guild.isRestricted ? 10 : "*"}) @@ -65,22 +69,15 @@ export const DiscordGuildList = (props: BoxProps) => { ))} - +
)) )} - ); }; diff --git a/src/features/Discord/SummonerSelection.tsx b/src/features/Discord/SummonerSelection.tsx index 73b13ae..81203f1 100644 --- a/src/features/Discord/SummonerSelection.tsx +++ b/src/features/Discord/SummonerSelection.tsx @@ -27,12 +27,14 @@ export const AddSummonerModal = ({ guildId, channelId, onClose, + isRestricted, }: { summoners: DiscordGuild["summoners"]; guildName: string; guildId: string; channelId: string; onClose: () => void; + isRestricted?: boolean; }) => { const { friendGroups } = useFriendList(); const [selection, api] = useSelection({ @@ -50,15 +52,16 @@ export const AddSummonerModal = ({ const summonersIds = summoners.map((summoner) => summoner.puuid); const selectionIds = selection.map((selected) => selected.puuid); + const { toAdd, toRemove } = selection.reduce( + (acc, current) => + summoners.find((summoner) => summoner.puuid === current.puuid) + ? { ...acc, toRemove: [...acc.toAdd, current] } + : { ...acc, toAdd: [...acc.toAdd, current] }, + { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } + ); + const currentNbSelected = summoners.length + toAdd.length - toRemove.length; const onClick = () => { - const { toAdd, toRemove } = selection.reduce( - (acc, current) => - summoners.find((summoner) => summoner.puuid === current.puuid) - ? { ...acc, toRemove: [...acc.toAdd, current] } - : { ...acc, toAdd: [...acc.toAdd, current] }, - { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } - ); if (toAdd.length) { window.Main.sendMessage("discord/add-friends", { channelId, @@ -81,7 +84,11 @@ export const AddSummonerModal = ({ {selection.length !== 0 && (
-
@@ -89,6 +96,20 @@ export const AddSummonerModal = ({ Add or remove summoners to {guildName} stalking list + {isRestricted && ( + + Stalker summoners: {currentNbSelected}/10 + + )} {meQuery.isSuccess && ( +export const getFriendRanks = (puuid: FriendDto["puuid"]) => electronRequest("friendList/friend", puuid); type FriendDetailsState = "notifications" | "match-history" | "graph" | "old-names"; diff --git a/src/features/FriendDetails/FriendRankingGraph.tsx b/src/features/FriendDetails/FriendRankingGraph.tsx index f392bc1..37a5d9f 100644 --- a/src/features/FriendDetails/FriendRankingGraph.tsx +++ b/src/features/FriendDetails/FriendRankingGraph.tsx @@ -27,7 +27,6 @@ import { formatRank } from "./FriendDetails"; export const FriendRankingGraph = ({ friend }: { friend: FriendDto }) => { const tierDataRef = useRef(null as any); const query = useQuery("config/apex", () => electronRequest("config/apex")); - console.log(friend); const data = useMemo(() => { if (!query.data) return []; tierDataRef.current = makeTierData(query.data); @@ -52,6 +51,7 @@ export const FriendRankingGraph = ({ friend }: { friend: FriendDto }) => { ); } + return ( diff --git a/src/features/FriendDetails/Profile.tsx b/src/features/FriendDetails/Profile.tsx index 4f84eb3..d3afb58 100644 --- a/src/features/FriendDetails/Profile.tsx +++ b/src/features/FriendDetails/Profile.tsx @@ -5,7 +5,6 @@ import { ProfileIcon } from "../DataDragon/Profileicon"; import { formatRank } from "./FriendDetails"; export const Profile = ({ friend }: { friend: FriendDto }) => { - console.log(friend); if (!friend?.rankings) return null; const lastRanking = last(friend?.rankings); return ( From d28278035198160aca1a808d2ffbd8701e86beef Mon Sep 17 00:00:00 2001 From: ledouxm Date: Fri, 11 Feb 2022 14:22:27 +0100 Subject: [PATCH 24/35] feat: set version to 0.1.0 --- package.json | 2 +- src/features/Notifications/Notifications.tsx | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index d14c88c..3e9990d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lol-stalker", - "version": "0.0.2", + "version": "0.1.0", "license": "MIT", "main": "main/electron/index.js", "author": { diff --git a/src/features/Notifications/Notifications.tsx b/src/features/Notifications/Notifications.tsx index f31f537..2a96c3f 100644 --- a/src/features/Notifications/Notifications.tsx +++ b/src/features/Notifications/Notifications.tsx @@ -26,14 +26,7 @@ export const Notifications = () => { ) : ( - + Recent notifications From 8cb2e400c548f40ab4eab3234820701d441073f6 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Fri, 11 Feb 2022 14:29:09 +0100 Subject: [PATCH 25/35] fix: get db url --- electron/features/store.ts | 2 +- electron/utils.ts | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/electron/features/store.ts b/electron/features/store.ts index 2e9e183..8da826d 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -10,7 +10,7 @@ import { WebSocket } from "ws"; export const initialConfig = { windowsNotifications: true, - dirname: __dirname, + dirname: path.join(__dirname, ".."), defaultLossMessage: "\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}\u{1F602}", }; diff --git a/electron/utils.ts b/electron/utils.ts index b0dbc7d..d04667b 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -114,11 +114,8 @@ export const getRankDifference = (oldRank: Rank, newRank: Rank) => { export const getDbPath = () => path.join( - __dirname, - path.join( - process.env.APPDATA!, - "LoL Stalker", - "database", - electronIsDev ? "lol-stalker.db" : "lol-stalker.dev.db" - ) + process.env.APPDATA!, + "LoL Stalker", + "database", + electronIsDev ? "lol-stalker.dev.db" : "lol-stalker.db" ); From 0a7e38b52f22d4b285ce5e4e204edb522a3025a6 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Sun, 13 Feb 2022 17:10:27 +0100 Subject: [PATCH 26/35] feat: ws reconnect loop --- electron/features/ws/discord.ts | 6 ++++++ electron/jobs/currentSummonerRank.ts | 2 +- src/features/CurrentSummoner/CurrentSummoner.tsx | 2 ++ src/features/Notifications/InGameFriends.tsx | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index bde5c10..0e16ca4 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -20,6 +20,7 @@ export const makeSocketClient = async () => { ) ); + let timeout = null as any as NodeJS.Timer; const socket = new WebSocket(wsUrl + "?" + search.toString(), {}); await editStoreEntry("socketStatus", "connecting"); await editStoreEntry("backendSocket", socket); @@ -27,6 +28,10 @@ export const makeSocketClient = async () => { socket.onopen = async () => { console.log("Connection opened"); await editStoreEntry("socketStatus", "connected"); + + timeout = setInterval(() => { + socket.send(JSON.stringify({ event: "ping", data: null })); + }, 10000); }; socket.onerror = async (error) => { console.error("WebSocket error"); @@ -39,6 +44,7 @@ export const makeSocketClient = async () => { console.log("WebSocket close"); await editStoreEntry("socketStatus", "closed"); console.log("Retrying in 3s..."); + clearTimeout(timeout); setTimeout(() => makeSocketClient(), 3000); }; } catch (error) { diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index b351c44..d4d2dd6 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -3,7 +3,7 @@ import { getManager } from "typeorm"; import { Friend } from "../entities/Friend"; import { Ranking } from "../entities/Ranking"; import { getCurrentSummoner, getSoloQRankedStats } from "../features/lcu/lcu"; -import { editStoreEntry } from "../features/store"; +import { editStoreEntry, store } from "../features/store"; import { sendWs } from "../features/ws/discord"; import { getRankDifference } from "../utils"; diff --git a/src/features/CurrentSummoner/CurrentSummoner.tsx b/src/features/CurrentSummoner/CurrentSummoner.tsx index 32b58b5..e7ffa6b 100644 --- a/src/features/CurrentSummoner/CurrentSummoner.tsx +++ b/src/features/CurrentSummoner/CurrentSummoner.tsx @@ -17,6 +17,8 @@ export const CurrentSummoner = () => { if (friendQuery.isLoading) return ; if (!friendQuery.data) return null; + if (!currentSummoner) return ; + return ( {currentSummoner && ( diff --git a/src/features/Notifications/InGameFriends.tsx b/src/features/Notifications/InGameFriends.tsx index cd53392..3252c83 100644 --- a/src/features/Notifications/InGameFriends.tsx +++ b/src/features/Notifications/InGameFriends.tsx @@ -63,7 +63,7 @@ const InGameFriendRow = ({ friend }: { friend: InGameFriend }) => { const champion = useChampionDataById(friend.championId); const navigate = useNavigate(); return ( - + Date: Mon, 14 Feb 2022 16:43:06 +0100 Subject: [PATCH 27/35] feat: add/remove summoners using api + friendlist search bar --- electron/features/store.ts | 9 +- electron/features/ws/discord.ts | 3 - src/api.ts | 16 ++ src/components/LCUConnector.tsx | 10 +- src/features/Discord/AddSummonerButton.tsx | 6 +- src/features/Discord/AddSummonerModal.tsx | 214 +++++++++++++++++++ src/features/Discord/BotInfos.tsx | 21 +- src/features/Discord/DiscordGuildList.tsx | 29 ++- src/features/Discord/SummonerPanel.tsx | 4 +- src/features/Discord/SummonerSelection.tsx | 168 --------------- src/features/FriendList/FriendList.tsx | 98 ++++++--- src/features/FriendList/SearchFriendList.tsx | 23 ++ src/features/hooks/useApi.ts | 15 ++ 13 files changed, 393 insertions(+), 223 deletions(-) create mode 100644 src/api.ts create mode 100644 src/features/Discord/AddSummonerModal.tsx delete mode 100644 src/features/Discord/SummonerSelection.tsx create mode 100644 src/features/FriendList/SearchFriendList.tsx create mode 100644 src/features/hooks/useApi.ts diff --git a/electron/features/store.ts b/electron/features/store.ts index 8da826d..90346ae 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -61,7 +61,7 @@ interface StoreConfig { persist?: boolean; notifyOnChange?: boolean; formatter?: (data: any) => any; - onLoad?: (data: any) => any; + formatOnLoad?: (data: any) => any; } const initialStore: Store = { config: initialConfig, @@ -99,14 +99,11 @@ const storeConfig: Partial> = { persist: true, notifyOnChange: true, formatter: (data: Set) => data && Array.from(data), - onLoad: (data: string[]) => data && new Set(data), + formatOnLoad: (data: string[]) => data && new Set(data), }, inGameFriends: { notifyOnChange: true, }, - userGuilds: { - notifyOnChange: true, - }, connectorStatus: { notifyOnChange: true, }, @@ -165,7 +162,7 @@ export const loadStore = async () => { try { const stored = JSON.parse(await fs.readFile(getJsonPath(entryName), "utf-8")); if (stored) { - store[entryName] = storeConfig[entryName]?.onLoad?.(stored) || stored; + store[entryName] = storeConfig[entryName]?.formatOnLoad?.(stored) || stored; } } catch (e) { console.log("Couldn't load ", entryName + ".json"); diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index 0e16ca4..b2343b4 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -86,9 +86,6 @@ const makeCallback: Record void> = { id: async (data: string) => { await editStoreEntry("config", { ...store.config, socketId: data }); }, - guilds: async (guilds: string[]) => { - await editStoreEntry("userGuilds", guilds); - }, auth: async (data: DiscordAuth) => { sendWs("guilds", { accessToken: data.access_token }); focusWindow(); diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..30caa45 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,16 @@ +import { isDev } from "@pastable/utils"; +import axios from "axios"; + +const baseURL = isDev() ? "http://localhost:8080" : `https://stalker.back.chainbreak.dev`; + +export const api = axios.create({ baseURL }); +api.interceptors.response.use( + (response) => response, + (error) => { + // If token has expired, force a full page refresh + if (error.response?.status === 401) { + // document.location.reload(); + } + throw error; + } +); diff --git a/src/components/LCUConnector.tsx b/src/components/LCUConnector.tsx index ac37446..040e826 100644 --- a/src/components/LCUConnector.tsx +++ b/src/components/LCUConnector.tsx @@ -1,14 +1,14 @@ -import { useInterval } from "@chakra-ui/react"; import axios from "axios"; import { atom } from "jotai"; -import { atomWithStorage, useUpdateAtom } from "jotai/utils"; +import { useUpdateAtom } from "jotai/utils"; import { useEffect } from "react"; import { useQuery, useQueryClient } from "react-query"; import { useChampionsList } from "../features/DataDragon/useChampionsList"; import { useItemsList } from "../features/DataDragon/useItemsList"; import { useSummonerSpellsList } from "../features/DataDragon/useSummonerSpellsList"; import { friendsAtom } from "../features/FriendList/useFriendList"; -import { AuthData, CurrentSummoner, FriendDto, FriendLastRankDto } from "../types"; +import { useApi } from "../features/hooks/useApi"; +import { CurrentSummoner, FriendLastRankDto } from "../types"; import { electronRequest, sendMessage } from "../utils"; import { errorToast } from "./toasts"; @@ -60,7 +60,6 @@ export interface Store { selectedFriends: Array | null; connectorStatus: null | ConnectorStatus; inGameFriends: null | any[]; - userGuilds: null | DiscordGuild[]; discordAuth: null | DiscordAuth; socketStatus: SocketStatus; discordUrls: null | DiscordUrls; @@ -69,8 +68,8 @@ export interface Store { } export const storeAtom = atom(null); + export const lcuStatusAtom = atom((get) => get(storeAtom)?.connectorStatus); -export const discordGuildsAtom = atom((get) => get(storeAtom)?.userGuilds); export const configAtom = atom((get) => get(storeAtom)?.config); export const socketStatusAtom = atom((get) => get(storeAtom)?.socketStatus); export const selectedFriendsAtom = atom((get) => get(storeAtom)?.selectedFriends); @@ -92,6 +91,7 @@ export const LCUConnector = () => { useChampionsList(); useItemsList(); useSummonerSpellsList(); + useApi(); useEffect(() => { window.Main.on("invalidate", (queryName: string) => diff --git a/src/features/Discord/AddSummonerButton.tsx b/src/features/Discord/AddSummonerButton.tsx index 90f8058..d9b0094 100644 --- a/src/features/Discord/AddSummonerButton.tsx +++ b/src/features/Discord/AddSummonerButton.tsx @@ -1,7 +1,7 @@ import { Center, chakra, Modal, useDisclosure } from "@chakra-ui/react"; import { BiMinusCircle, BiPlusCircle } from "react-icons/bi"; import { DiscordGuild } from "../../components/LCUConnector"; -import { AddSummonerModal } from "./SummonerSelection"; +import { AddSummonerModal, useRemoveSummonersMutation } from "./AddSummonerModal"; export const AddSummonerButton = ({ guildId, @@ -11,7 +11,7 @@ export const AddSummonerButton = ({ isRestricted, }: DiscordGuild) => { const disclosure = useDisclosure(); - + const removeSummonersMutation = useRemoveSummonersMutation(); return ( <>
@@ -24,7 +24,7 @@ export const AddSummonerButton = ({ ml="20px" cursor="pointer" onClick={() => { - window.Main.sendMessage("discord/remove-friends", { + removeSummonersMutation.mutate({ channelId: channelId, guildId: guildId, summoners: summoners.map((summoner) => summoner.puuid), diff --git a/src/features/Discord/AddSummonerModal.tsx b/src/features/Discord/AddSummonerModal.tsx new file mode 100644 index 0000000..76c2a9f --- /dev/null +++ b/src/features/Discord/AddSummonerModal.tsx @@ -0,0 +1,214 @@ +import { + Accordion, + AccordionButton, + AccordionItem, + AccordionPanel, + Box, + Button, + Center, + Flex, + Icon, + ModalCloseButton, + ModalContent, + Stack, +} from "@chakra-ui/react"; +import { useSelection } from "@pastable/core"; +import { useState } from "react"; +import { BiFolder } from "react-icons/bi"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { api } from "../../api"; +import { DiscordGuild } from "../../components/LCUConnector"; +import { FriendLastRankDto } from "../../types"; +import { electronRequest } from "../../utils"; +import { useSearchFriendlist } from "../FriendList/FriendList"; +import { SearchFriendlist } from "../FriendList/SearchFriendList"; +import { useFriendList } from "../FriendList/useFriendList"; +import { getBgColor } from "./discordUtils"; + +type SummonerSelection = Omit; +export const useRemoveSummonersMutation = () => { + const queryClient = useQueryClient(); + return useMutation((payload: any) => api.post("/remove-summoners", payload), { + onSuccess: () => queryClient.invalidateQueries("guilds"), + }); +}; +export const AddSummonerModal = ({ + summoners, + guildName, + guildId, + channelId, + onClose, + isRestricted, +}: { + summoners: DiscordGuild["summoners"]; + guildName: string; + guildId: string; + channelId: string; + onClose: () => void; + isRestricted?: boolean; +}) => { + const { friendGroups } = useFriendList(); + const [selection, selectionApi] = useSelection({ + getId: (item) => item.puuid, + }); + + const meQuery = useQuery("me", () => electronRequest("me")); + const queryClient = useQueryClient(); + const addSummonersMutation = useMutation( + (payload: any) => api.post("/add-summoners", payload), + { onSuccess: () => queryClient.invalidateQueries("guilds") } + ); + const removeSummonersMutation = useRemoveSummonersMutation(); + + const [search, setSearch] = useState(""); + + const searchFriendlist = useSearchFriendlist(friendGroups, search); + + if (!friendGroups?.length) + return ( +
+ No friend. You can try refreshing the page (CTRL-R) +
+ ); + + const summonersIds = summoners.map((summoner) => summoner.puuid); + const selectionIds = selection.map((selected) => selected.puuid); + const { toAdd, toRemove } = selection.reduce( + (acc, current) => + summoners.find((summoner) => summoner.puuid === current.puuid) + ? { ...acc, toRemove: [...acc.toAdd, current] } + : { ...acc, toAdd: [...acc.toAdd, current] }, + { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } + ); + const currentNbSelected = summoners.length + toAdd.length - toRemove.length; + + const onClick = () => { + if (toAdd.length) { + const payload = { channelId, guildId, summoners: toAdd }; + addSummonersMutation.mutate(payload); + } + if (toRemove.length) { + removeSummonersMutation.mutate({ + channelId, + guildId, + summoners: toRemove.map((summoner) => summoner.puuid), + }); + } + onClose(); + }; + + return ( + + + {selection.length !== 0 && ( +
+ +
+ )} + + Add or remove summoners to {guildName} stalking list + + {isRestricted && ( + + Stalker summoners: {currentNbSelected}/10 + + )} + + {search && + searchFriendlist.map((friend) => ( + selectionApi.toggle(friend)} + /> + ))} + {!search && ( + + {meQuery.isSuccess && ( + + selectionApi.toggle({ + name: meQuery.data.displayName, + puuid: meQuery.data.puuid, + id: 0, + }) + } + > + Me ({meQuery.data.displayName}) + + )} + + {friendGroups.map((group) => ( + + + + + {group.groupName} + + + + + {group.friends.map((friend) => ( + selectionApi.toggle(friend)} + /> + ))} + + + + ))} + + + )} +
+ ); +}; + +const FriendRow = ({ + summonersIds, + selectionIds, + onClick, + ...friend +}: FriendLastRankDto & { summonersIds: string[]; selectionIds: string[]; onClick: () => void }) => { + return ( + + + {friend.name} + + + ); +}; diff --git a/src/features/Discord/BotInfos.tsx b/src/features/Discord/BotInfos.tsx index 6a421c9..f15b70e 100644 --- a/src/features/Discord/BotInfos.tsx +++ b/src/features/Discord/BotInfos.tsx @@ -8,23 +8,40 @@ import { Icon, IconButton, ListItem, + Spinner, UnorderedList, } from "@chakra-ui/react"; import { useAtomValue } from "jotai/utils"; import { BiLogOut } from "react-icons/bi"; import { FaDiscord } from "react-icons/fa"; -import { discordGuildsAtom, meAtom } from "../../components/LCUConnector"; +import { meAtom } from "../../components/LCUConnector"; import { electronMutation } from "../../utils"; import { BotInvitation } from "./BotInvitation"; +import { useGuildsQuery } from "./DiscordGuildList"; export const BotInfos = (props: BoxProps) => { - const guilds = useAtomValue(discordGuildsAtom); + const guildsQuery = useGuildsQuery(); const me = useAtomValue(meAtom); + if (guildsQuery.isLoading) + return ( +
+ +
+ ); + if (guildsQuery.isError) + return ( +
+ An error has occured +
+ ); + + const guilds = guildsQuery.data; return (
(await api.get("/guilds")).data; +export const useGuildsQuery = () => + useQuery("guilds", getGuilds, { staleTime: 1000 * 60 * 5 }); export const DiscordGuildList = (props: BoxProps) => { - const guilds = useAtomValue(discordGuildsAtom); + const guildsQuery = useGuildsQuery(); - useEffect(() => { - if (!guilds) refreshGuilds(); - }, [guilds]); + if (guildsQuery.isLoading) + return ( +
+ +
+ ); + if (guildsQuery.isError) + return ( +
+ An error has occured +
+ ); + const guilds = guildsQuery.data!; console.log(guilds); return ( - + Stalked summoners diff --git a/src/features/Discord/SummonerPanel.tsx b/src/features/Discord/SummonerPanel.tsx index c5fb6cb..c3cc21c 100644 --- a/src/features/Discord/SummonerPanel.tsx +++ b/src/features/Discord/SummonerPanel.tsx @@ -2,6 +2,7 @@ import { CloseIcon } from "@chakra-ui/icons"; import { Box, Flex } from "@chakra-ui/react"; import { DiscordGuild } from "../../components/LCUConnector"; import { electronMutation } from "../../utils"; +import { useRemoveSummonersMutation } from "./AddSummonerModal"; export const SummonerPanel = ({ summoner, @@ -12,6 +13,7 @@ export const SummonerPanel = ({ guildId: string; channelId: string; }) => { + const removeSummonersMutation = useRemoveSummonersMutation(); return ( {summoner.name} @@ -19,7 +21,7 @@ export const SummonerPanel = ({ boxSize="15px" cursor="pointer" onClick={() => - electronMutation("discord/remove-friends", { + removeSummonersMutation.mutate({ guildId, channelId, summoners: [summoner.puuid], diff --git a/src/features/Discord/SummonerSelection.tsx b/src/features/Discord/SummonerSelection.tsx deleted file mode 100644 index 81203f1..0000000 --- a/src/features/Discord/SummonerSelection.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { - Accordion, - AccordionButton, - AccordionItem, - AccordionPanel, - Box, - Button, - Center, - Flex, - Icon, - ModalCloseButton, - ModalContent, - Stack, -} from "@chakra-ui/react"; -import { useSelection } from "@pastable/core"; -import { BiFolder } from "react-icons/bi"; -import { useQuery } from "react-query"; -import { DiscordGuild } from "../../components/LCUConnector"; -import { electronRequest } from "../../utils"; -import { useFriendList } from "../FriendList/useFriendList"; -import { getBgColor } from "./discordUtils"; - -type SummonerSelection = Omit; -export const AddSummonerModal = ({ - summoners, - guildName, - guildId, - channelId, - onClose, - isRestricted, -}: { - summoners: DiscordGuild["summoners"]; - guildName: string; - guildId: string; - channelId: string; - onClose: () => void; - isRestricted?: boolean; -}) => { - const { friendGroups } = useFriendList(); - const [selection, api] = useSelection({ - getId: (item) => item.puuid, - }); - - const meQuery = useQuery("me", () => electronRequest("me")); - - if (!friendGroups?.length) - return ( -
- No friend. You can try refreshing the page (CTRL-R) -
- ); - - const summonersIds = summoners.map((summoner) => summoner.puuid); - const selectionIds = selection.map((selected) => selected.puuid); - const { toAdd, toRemove } = selection.reduce( - (acc, current) => - summoners.find((summoner) => summoner.puuid === current.puuid) - ? { ...acc, toRemove: [...acc.toAdd, current] } - : { ...acc, toAdd: [...acc.toAdd, current] }, - { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } - ); - const currentNbSelected = summoners.length + toAdd.length - toRemove.length; - - const onClick = () => { - if (toAdd.length) { - window.Main.sendMessage("discord/add-friends", { - channelId, - guildId, - summoners: toAdd, - }); - } - if (toRemove.length) { - window.Main.sendMessage("discord/remove-friends", { - channelId, - guildId, - summoners: toRemove.map((summoner) => summoner.puuid), - }); - } - onClose(); - }; - - return ( - - - {selection.length !== 0 && ( -
- -
- )} - - Add or remove summoners to {guildName} stalking list - - {isRestricted && ( - - Stalker summoners: {currentNbSelected}/10 - - )} - {meQuery.isSuccess && ( - - api.toggle({ - name: meQuery.data.displayName, - puuid: meQuery.data.puuid, - id: 0, - }) - } - > - Me ({meQuery.data.displayName}) - - )} - - {friendGroups.map((group) => ( - - - - - {group.groupName} - - - - - {group.friends.map((friend) => ( - - api.toggle(friend)} - > - {friend.name} - - - ))} - - - - ))} - -
- ); -}; diff --git a/src/features/FriendList/FriendList.tsx b/src/features/FriendList/FriendList.tsx index b3073d1..83046b8 100644 --- a/src/features/FriendList/FriendList.tsx +++ b/src/features/FriendList/FriendList.tsx @@ -1,3 +1,4 @@ +import { SearchIcon } from "@chakra-ui/icons"; import { Accordion, Box, @@ -6,20 +7,42 @@ import { chakra, Divider, Flex, + Input, ListItem, - Spinner, Stack, UnorderedList, } from "@chakra-ui/react"; import { useAtomValue } from "jotai/utils"; -import { leagueSummonerAtom } from "../../components/LCUConnector"; +import { useState } from "react"; +import { leagueSummonerAtom, selectedFriendsAtom } from "../../components/LCUConnector"; +import { FriendGroup, FriendLastRankDto } from "../../types"; import { ProfileIcon } from "../DataDragon/Profileicon"; -import { FriendGroupRow } from "./FriendGroup"; +import { FriendGroupRow, FriendRow } from "./FriendGroup"; +import { SearchFriendlist } from "./SearchFriendList"; import { useFriendList } from "./useFriendList"; +export const useSearchFriendlist = (friendGroups: FriendGroup[], search?: string) => { + const matchingFriends: FriendGroup["friends"] = []; + if (!search) return matchingFriends; + + friendGroups.forEach((group) => + group.friends.forEach((friend) => { + if (friend.name.toLowerCase().includes(search.toLowerCase())) + matchingFriends.push(friend); + }) + ); + + return matchingFriends; +}; + export const FriendList = () => { const { friendGroups } = useFriendList(); const leagueSummoner = useAtomValue(leagueSummonerAtom); + const [search, setSearch] = useState(""); + + const searchFriendlist = useSearchFriendlist(friendGroups, search); + const selectedFriends = useAtomValue(selectedFriendsAtom); + if (!friendGroups?.length) return (
@@ -64,33 +87,50 @@ export const FriendList = () => { Friendlist - - + + + )} + {search && ( + + {searchFriendlist.map((friend) => ( + + ))} + + )} + {!search && ( + - Select all - - - - - {friendGroups?.map((group) => ( - - ))} - + {friendGroups?.map((group) => ( + + ))} + + )} ); diff --git a/src/features/FriendList/SearchFriendList.tsx b/src/features/FriendList/SearchFriendList.tsx new file mode 100644 index 0000000..41579c5 --- /dev/null +++ b/src/features/FriendList/SearchFriendList.tsx @@ -0,0 +1,23 @@ +import { SearchIcon } from "@chakra-ui/icons"; +import { Box, Input } from "@chakra-ui/react"; +import { SetState } from "@pastable/core"; + +export const SearchFriendlist = ({ + setSearch, + search, +}: { + setSearch: SetState; + search: string; +}) => { + return ( + + setSearch(e.target.value)} + value={search} + /> + + + ); +}; diff --git a/src/features/hooks/useApi.ts b/src/features/hooks/useApi.ts new file mode 100644 index 0000000..9fab35d --- /dev/null +++ b/src/features/hooks/useApi.ts @@ -0,0 +1,15 @@ +import { useAtomValue } from "jotai/utils"; +import { useEffect } from "react"; +import { api } from "../../api"; +import { discordAuthAtom } from "../../components/LCUConnector"; + +export const useApi = () => { + const discordAuth = useAtomValue(discordAuthAtom); + + useEffect(() => { + if (!discordAuth) return void (api.defaults.headers.common["authorization"] = ""); + api.defaults.headers.common["authorization"] = "Bearer " + discordAuth.access_token; + }, [discordAuth]); + + return null; +}; From d4e17344e861df5f67a933d069cccd20738e6562 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Mon, 14 Feb 2022 17:26:19 +0100 Subject: [PATCH 28/35] feat: add promo notifications --- electron/features/lcu/lcu.ts | 3 ++- electron/jobs/currentSummonerRank.ts | 3 ++- electron/utils.ts | 25 ++++++++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/electron/features/lcu/lcu.ts b/electron/features/lcu/lcu.ts index 06d921a..fe23820 100644 --- a/electron/features/lcu/lcu.ts +++ b/electron/features/lcu/lcu.ts @@ -51,7 +51,8 @@ export const compareFriends = async (oldFriends: FriendStats[], newFriends: Frie if ( newFriend.division !== oldFriend.division || newFriend.tier !== oldFriend.tier || - newFriend.leaguePoints !== oldFriend.leaguePoints + newFriend.leaguePoints !== oldFriend.leaguePoints || + newFriend.miniSeriesProgress !== oldFriend.miniSeriesProgress ) { changes.push({ ...newFriend, diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index d4d2dd6..014c22d 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -41,7 +41,8 @@ export const startCheckCurrentSummonerRank = async () => { !lastRanking || lastRanking.division !== summonerRank.division || lastRanking.tier !== summonerRank.tier || - lastRanking.leaguePoints !== summonerRank.leaguePoints + lastRanking.leaguePoints !== summonerRank.leaguePoints || + lastRanking.miniSeriesProgress !== summonerRank?.miniSeriesProgress ) { console.log("saving new ranking"); const ranking = manager.create(Ranking, { diff --git a/electron/utils.ts b/electron/utils.ts index d04667b..1c383e7 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -14,10 +14,28 @@ export const sendToClient = (channel: string, ...args: any[]) => export const makeDataDragonUrl = (buildVersion: string) => `https://ddragon.leagueoflegends.com/cdn/dragontail-${buildVersion}.tgz`; -export const formatRank = (ranking: Pick) => - `${ranking.tier}${ranking.division !== "NA" ? ` ${ranking.division}` : ""} - ${ +export const formatRank = ( + ranking: Pick & { miniSeriesProgress?: string } +) => { + const isPromo = !!ranking.miniSeriesProgress && ranking.miniSeriesProgress !== "NNNNN"; + + return `${ranking.tier}${ranking.division !== "NA" ? ` ${ranking.division}` : ""} - ${ ranking.leaguePoints - } LPs`; + } LPs${isPromo ? " " + getPromosGames(ranking.miniSeriesProgress!) : ""}`; +}; + +const getPromosGames = (miniSeriesProgress: string) => { + const nbWin = Array.from(miniSeriesProgress).reduce( + (acc, current) => (current === "W" ? acc + 1 : acc), + 0 + ); + const nbLoss = Array.from(miniSeriesProgress).reduce( + (acc, current) => (current === "L" ? acc + 1 : acc), + 0 + ); + + return `(${nbWin} - ${nbLoss})`; +}; export const ranks: Rank[] = [ { @@ -65,6 +83,7 @@ interface Rank { tier: string; division: string; leaguePoints: number; + miniSeriesProgress?: string; } const tiers = [ "IRON", From 19ba3cacf56406414c15adb8fd6ad9627c28b57c Mon Sep 17 00:00:00 2001 From: ledouxm Date: Mon, 14 Feb 2022 17:26:51 +0100 Subject: [PATCH 29/35] fix: update apex every 15 minutes --- electron/jobs/updateApex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/jobs/updateApex.ts b/electron/jobs/updateApex.ts index 1999a0a..4424d33 100644 --- a/electron/jobs/updateApex.ts +++ b/electron/jobs/updateApex.ts @@ -8,7 +8,7 @@ export const startUpdateApex = async () => { sendWs("apex", apex); console.log("sent apex to ws backed"); - setTimeout(() => startUpdateApex(), 1000 * 60); + setTimeout(() => startUpdateApex(), 1000 * 60 * 15); } catch (e) { console.log("couldn't find apex, retrying in 5s"); setTimeout(() => startUpdateApex(), 5000); From e58a51b54b3d75e3d886045efd6e133f2c943863 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Mon, 14 Feb 2022 17:30:47 +0100 Subject: [PATCH 30/35] feat: add message button if 99 LPs --- src/features/Notifications/NotificationItem.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/features/Notifications/NotificationItem.tsx b/src/features/Notifications/NotificationItem.tsx index dd8fe80..8c49f03 100644 --- a/src/features/Notifications/NotificationItem.tsx +++ b/src/features/Notifications/NotificationItem.tsx @@ -44,7 +44,7 @@ export const NotificationItem = ({ {formatNotificationContent(notification)} - {["DEMOTION", "LOSS"].includes(notification.type) && ( + {hasMessageButton(notification) && ( { + return ( + ["DEMOTION", "LOSS"].includes(notification.type) || + (notification.to.includes(" 99 LPs") && + ["MASTER", "GRANDMASTER", "CHALLENGER"].every( + (tier) => !notification.to.includes(tier) + )) + ); +}; + const formatNotificationContent = (notification: NotificationDto) => { return notification.content.replace(" NA ", " "); }; From 75ef78d499f6f0b661f201ce140f84fa61fe0130 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Mon, 14 Feb 2022 17:31:26 +0100 Subject: [PATCH 31/35] fix: remove spectating from in game friends --- electron/features/routes/friends.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electron/features/routes/friends.ts b/electron/features/routes/friends.ts index dc08c5b..5b76768 100644 --- a/electron/features/routes/friends.ts +++ b/electron/features/routes/friends.ts @@ -106,7 +106,9 @@ export const addOrUpdateFriends = async (friends: FriendDto[]) => { for (const friend of friends) { if ( - !["outOfGame", "hosting_RANKED_SOLO_5x5"].includes(friend.lol.gameStatus) && + !["outOfGame", "hosting_RANKED_SOLO_5x5", "spectating"].includes( + friend.lol.gameStatus + ) && friend.lol.gameQueueType === "RANKED_SOLO_5x5" ) { currentInGame.push({ From 0e0753dfcdd2a759fc19076ef7dcc541474ffb34 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Mon, 14 Feb 2022 18:00:20 +0100 Subject: [PATCH 32/35] feat: miniseriesprogress --- electron/jobs/friendListJob.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index c55f2f6..8835418 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -40,13 +40,15 @@ export const startCheckFriendListJob = async () => { apexFromLCU[change.tier as Tier]; const payload = { - ...notification, + // ...notification, fromDivision: change.oldFriend.division, fromTier: change.oldFriend.tier, fromLeaguePoints: change.oldFriend.leaguePoints, + fromMiniSeriesProgress: change.oldFriend.miniSeriesProgress, toDivision: change.division, toTier: change.tier, toLeaguePoints: change.leaguePoints, + toMiniSeriesProgress: change.miniSeriesProgress, puuid: change.puuid, name: change.name, apex, From fbeb351f13961abae73ce3972249994262738bc6 Mon Sep 17 00:00:00 2001 From: ledouxm Date: Wed, 16 Feb 2022 12:38:51 +0100 Subject: [PATCH 33/35] feat: is in friendlist + fix api --- electron/features/ws/discord.ts | 6 ++-- src/components/LCUConnector.tsx | 3 +- src/features/Discord/AddSummonerModal.tsx | 13 +++++---- src/features/Discord/BotInfos.tsx | 8 +----- src/features/Discord/DiscordGuildList.tsx | 35 +++++++++++++++-------- src/features/Discord/SummonerPanel.tsx | 17 +++++++++-- 6 files changed, 51 insertions(+), 31 deletions(-) diff --git a/electron/features/ws/discord.ts b/electron/features/ws/discord.ts index b2343b4..3b191ce 100644 --- a/electron/features/ws/discord.ts +++ b/electron/features/ws/discord.ts @@ -2,6 +2,7 @@ import DiscordOauth2 from "discord-oauth2"; import WebSocket from "ws"; import { focusWindow } from "../.."; import { sendToClient, wsUrl } from "../../utils"; +import { sendInvalidate } from "../routes"; import { DiscordAuth, editStoreEntry, store } from "../store"; export const makeSocketClient = async () => { try { @@ -95,11 +96,10 @@ const makeCallback: Record void> = { await editStoreEntry("me", data); }, summoners: async (data) => console.log(data), - "summoners/add": async () => sendWs("guilds", { accessToken: store.discordAuth?.access_token }), - "summoners/remove": async () => - sendWs("guilds", { accessToken: store.discordAuth?.access_token }), + invalidateGuilds: () => sendInvalidate("guilds"), discordUrls: async (data) => editStoreEntry("discordUrls", data), invalidToken: () => editStoreEntry("discordAuth", null), + rateLimit: () => sendToClient("errorToast", "Rate limit error, try again later"), error: async (data) => sendToClient("error", data), }; diff --git a/src/components/LCUConnector.tsx b/src/components/LCUConnector.tsx index 040e826..74e9ae5 100644 --- a/src/components/LCUConnector.tsx +++ b/src/components/LCUConnector.tsx @@ -15,8 +15,8 @@ import { errorToast } from "./toasts"; export interface DiscordGuild { channelId: string; guildId: string; - name: string; channelName: string; + guildName: string; nbStalkers: number; summoners: { id: number; puuid: string; channelId: string; name: string }[]; isRestricted: boolean; @@ -104,6 +104,7 @@ export const LCUConnector = () => { window.Main.on("error", (data: string) => errorToast({ title: "An error has occured", description: data }) ); + window.Main.on("errorToast", (data: string) => errorToast({ title: data })); sendMessage("friendList/lastRank"); return () => diff --git a/src/features/Discord/AddSummonerModal.tsx b/src/features/Discord/AddSummonerModal.tsx index 76c2a9f..6a57791 100644 --- a/src/features/Discord/AddSummonerModal.tsx +++ b/src/features/Discord/AddSummonerModal.tsx @@ -32,6 +32,7 @@ export const useRemoveSummonersMutation = () => { onSuccess: () => queryClient.invalidateQueries("guilds"), }); }; + export const AddSummonerModal = ({ summoners, guildName, @@ -76,24 +77,26 @@ export const AddSummonerModal = ({ const { toAdd, toRemove } = selection.reduce( (acc, current) => summoners.find((summoner) => summoner.puuid === current.puuid) - ? { ...acc, toRemove: [...acc.toAdd, current] } + ? { ...acc, toRemove: [...acc.toRemove, current] } : { ...acc, toAdd: [...acc.toAdd, current] }, { toAdd: [] as SummonerSelection[], toRemove: [] as SummonerSelection[] } ); const currentNbSelected = summoners.length + toAdd.length - toRemove.length; const onClick = () => { - if (toAdd.length) { - const payload = { channelId, guildId, summoners: toAdd }; - addSummonersMutation.mutate(payload); - } if (toRemove.length) { + console.log(toRemove); removeSummonersMutation.mutate({ channelId, guildId, summoners: toRemove.map((summoner) => summoner.puuid), }); } + if (toAdd.length) { + console.log(toAdd); + const payload = { channelId, guildId, summoners: toAdd }; + addSummonersMutation.mutate(payload); + } onClose(); }; diff --git a/src/features/Discord/BotInfos.tsx b/src/features/Discord/BotInfos.tsx index f15b70e..534a7a3 100644 --- a/src/features/Discord/BotInfos.tsx +++ b/src/features/Discord/BotInfos.tsx @@ -22,18 +22,12 @@ import { useGuildsQuery } from "./DiscordGuildList"; export const BotInfos = (props: BoxProps) => { const guildsQuery = useGuildsQuery(); const me = useAtomValue(meAtom); - if (guildsQuery.isLoading) + if (!me) return (
); - if (guildsQuery.isError) - return ( -
- An error has occured -
- ); const guilds = guildsQuery.data; diff --git a/src/features/Discord/DiscordGuildList.tsx b/src/features/Discord/DiscordGuildList.tsx index 5c98f71..eea19c5 100644 --- a/src/features/Discord/DiscordGuildList.tsx +++ b/src/features/Discord/DiscordGuildList.tsx @@ -13,22 +13,20 @@ import { Spinner, Stack, } from "@chakra-ui/react"; -import { useAtomValue } from "jotai/utils"; -import { useEffect } from "react"; import { BiRefresh } from "react-icons/bi"; -import { DiscordGuild } from "../../components/LCUConnector"; -import { refreshGuilds } from "./Discord"; -import { SummonerPanel } from "./SummonerPanel"; -import { AddSummonerButton } from "./AddSummonerButton"; -import { LockIcon } from "@chakra-ui/icons"; import { useQuery } from "react-query"; import { api } from "../../api"; +import { DiscordGuild } from "../../components/LCUConnector"; +import { useFriendList } from "../FriendList/useFriendList"; +import { AddSummonerButton } from "./AddSummonerButton"; +import { SummonerPanel } from "./SummonerPanel"; const getGuilds = async () => (await api.get("/guilds")).data; export const useGuildsQuery = () => useQuery("guilds", getGuilds, { staleTime: 1000 * 60 * 5 }); export const DiscordGuildList = (props: BoxProps) => { const guildsQuery = useGuildsQuery(); + const { friends } = useFriendList(); if (guildsQuery.isLoading) return ( @@ -44,8 +42,6 @@ export const DiscordGuildList = (props: BoxProps) => { ); const guilds = guildsQuery.data!; - console.log(guilds); - return ( @@ -54,12 +50,24 @@ export const DiscordGuildList = (props: BoxProps) => { refreshGuilds()} + onClick={() => guildsQuery.refetch()} icon={} aria-label="Refresh guilds" /> - + + {/* {true && ( +
+ +
+ )} */} {!guilds?.length ? ( No guild ) : ( @@ -68,7 +76,7 @@ export const DiscordGuildList = (props: BoxProps) => { - {guild.name} - {guild.channelName}{" "} + {guild.guildName} - {guild.channelName}{" "} ({guild.summoners.length}/ {guild.isRestricted ? 10 : "*"}) @@ -86,6 +94,9 @@ export const DiscordGuildList = (props: BoxProps) => { friend.puuid === summoner.puuid + )} {...guild} /> ))} diff --git a/src/features/Discord/SummonerPanel.tsx b/src/features/Discord/SummonerPanel.tsx index c3cc21c..46a4caf 100644 --- a/src/features/Discord/SummonerPanel.tsx +++ b/src/features/Discord/SummonerPanel.tsx @@ -1,22 +1,33 @@ import { CloseIcon } from "@chakra-ui/icons"; -import { Box, Flex } from "@chakra-ui/react"; +import { Box, Flex, Icon, Tooltip } from "@chakra-ui/react"; +import { FaUserFriends } from "react-icons/fa"; import { DiscordGuild } from "../../components/LCUConnector"; -import { electronMutation } from "../../utils"; import { useRemoveSummonersMutation } from "./AddSummonerModal"; export const SummonerPanel = ({ summoner, guildId, channelId, + isInFriendList, }: { summoner: DiscordGuild["summoners"][0]; guildId: string; channelId: string; + isInFriendList?: boolean; }) => { const removeSummonersMutation = useRemoveSummonersMutation(); return ( - {summoner.name} + + {summoner.name} + {isInFriendList && ( + + + + + + )} + Date: Wed, 16 Feb 2022 14:11:12 +0100 Subject: [PATCH 34/35] fix --- electron/jobs/currentSummonerRank.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index 014c22d..5cdc292 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -60,15 +60,17 @@ export const startCheckCurrentSummonerRank = async () => { if (lastRanking) { const diff = getRankDifference(lastRanking, ranking); const payload = { - ...diff, + // ...diff, puuid: currentSummonerFromLCU.puuid, name: currentSummonerFromLCU.displayName, fromTier: lastRanking.tier, fromDivision: lastRanking.division, fromLeaguePoints: lastRanking.leaguePoints, + fromMiniSeriesProgress: lastRanking.miniSeriesProgress, toTier: ranking.tier, toDivision: ranking.division, toLeaguePoints: ranking.leaguePoints, + toMiniSeriesProgress: ranking.miniSeriesProgress, }; sendWs("update", payload); } From f0be75a85d73fc52ec7307e2b97677a1412b794c Mon Sep 17 00:00:00 2001 From: ledouxm Date: Fri, 18 Feb 2022 18:43:49 +0100 Subject: [PATCH 35/35] wip --- electron/features/lcu/lcu.ts | 10 +++++++- electron/features/store.ts | 30 +++++++++++++++++++++- electron/jobs/currentSummonerRank.ts | 3 +-- electron/jobs/friendListJob.ts | 2 +- src/components/LCUConnector.tsx | 12 ++++++++- src/features/Discord/AddSummonerButton.tsx | 6 ++--- src/features/Discord/AddSummonerModal.tsx | 15 +++++++---- src/features/Discord/DiscordGuildList.tsx | 2 +- src/features/Discord/SummonerPanel.tsx | 9 ++++--- 9 files changed, 71 insertions(+), 18 deletions(-) diff --git a/electron/features/lcu/lcu.ts b/electron/features/lcu/lcu.ts index fe23820..14de3e7 100644 --- a/electron/features/lcu/lcu.ts +++ b/electron/features/lcu/lcu.ts @@ -5,7 +5,7 @@ import LCUConnector from "lcu-connector"; import { Friend } from "../../entities/Friend"; import { sendToClient, Tier } from "../../utils"; import { CurrentSummoner, FriendDto, MatchDto, Queue, RankedStats } from "./types"; -import { editStoreEntry, store } from "../store"; +import { editStoreEntry, Locale, store } from "../store"; import { addOrUpdateFriends } from "../routes/friends"; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); @@ -23,6 +23,13 @@ connector.on("connect", async (data) => { headers: { Authorization: `Basic ${data.password}` }, }); console.log("connected to riot client"); + + try { + const locale = await getRegionLocale(); + await editStoreEntry("locale", locale); + } catch (e) { + console.log(e); + } }); connector.on("disconnect", () => { editStoreEntry("connectorStatus", null); @@ -98,6 +105,7 @@ export const getApexLeague = (tier: RankedStats["queues"][0]["tier"]) => ///lol-ranked-stats/v1/stats/{summonerId} export const getHelp = () => request("/help?format=Console"); export const getBuild = () => request("/system/v1/builds"); +export const getRegionLocale = () => request("/riotclient/region-locale"); export const getCurrentSummoner = () => request("/lol-summoner/v1/current-summoner"); export const getFriends = () => request("/lol-chat/v1/friends"); diff --git a/electron/features/store.ts b/electron/features/store.ts index 90346ae..57fac92 100644 --- a/electron/features/store.ts +++ b/electron/features/store.ts @@ -40,6 +40,28 @@ export interface DiscordUrls { inviteUrl: string; authUrl: string; } + +export interface Locale { + locale: string; + region: string; + webLanguage: string; + webRegion: string; +} + +export interface Me { + id: string; + username: string; + avatar: string; + discriminator: string; + public_flags: number; + flags: number; + banner?: any; + banner_color?: any; + accent_color?: any; + locale: string; + mfa_enabled: boolean; + premium_type: number; +} export interface Store { config: Record; selectedFriends: Set | null; @@ -53,8 +75,9 @@ export interface Store { socketStatus: SocketStatus; discordUrls: null | DiscordUrls; leagueSummoner: null | CurrentSummoner; - me: any | null; + me: Me | null; autoLaunch: AutoLaunch | null; + locale: Locale | null; } interface StoreConfig { @@ -78,6 +101,7 @@ const initialStore: Store = { leagueSummoner: null, me: null, autoLaunch: null, + locale: null, }; const resetStore = () => Object.entries(initialStore).forEach(([key, value]) => (store[key as keyof Store] = value)); @@ -117,6 +141,10 @@ const storeConfig: Partial> = { leagueSummoner: { notifyOnChange: true, }, + locale: { + notifyOnChange: true, + persist: true, + }, }; export const editStoreEntry = async ( diff --git a/electron/jobs/currentSummonerRank.ts b/electron/jobs/currentSummonerRank.ts index 5cdc292..3af6636 100644 --- a/electron/jobs/currentSummonerRank.ts +++ b/electron/jobs/currentSummonerRank.ts @@ -58,9 +58,8 @@ export const startCheckCurrentSummonerRank = async () => { }); await manager.save(ranking); if (lastRanking) { - const diff = getRankDifference(lastRanking, ranking); const payload = { - // ...diff, + region: store.locale?.region, puuid: currentSummonerFromLCU.puuid, name: currentSummonerFromLCU.displayName, fromTier: lastRanking.tier, diff --git a/electron/jobs/friendListJob.ts b/electron/jobs/friendListJob.ts index 8835418..09b56a3 100644 --- a/electron/jobs/friendListJob.ts +++ b/electron/jobs/friendListJob.ts @@ -40,7 +40,7 @@ export const startCheckFriendListJob = async () => { apexFromLCU[change.tier as Tier]; const payload = { - // ...notification, + region: store.locale?.region, fromDivision: change.oldFriend.division, fromTier: change.oldFriend.tier, fromLeaguePoints: change.oldFriend.leaguePoints, diff --git a/src/components/LCUConnector.tsx b/src/components/LCUConnector.tsx index 74e9ae5..2f055f4 100644 --- a/src/components/LCUConnector.tsx +++ b/src/components/LCUConnector.tsx @@ -18,7 +18,7 @@ export interface DiscordGuild { channelName: string; guildName: string; nbStalkers: number; - summoners: { id: number; puuid: string; channelId: string; name: string }[]; + summoners: { id: number; puuid: string; channelId: string; name: string; region: string }[]; isRestricted: boolean; } export interface ConnectorStatus { @@ -55,6 +55,14 @@ export interface Me { mfa_enabled: boolean; premium_type: number; } + +export interface Locale { + locale: string; + region: string; + webLanguage: string; + webRegion: string; +} + export interface Store { config: Record; selectedFriends: Array | null; @@ -65,6 +73,7 @@ export interface Store { discordUrls: null | DiscordUrls; leagueSummoner: null | CurrentSummoner; me: null | Me; + locale: null | Locale; } export const storeAtom = atom(null); @@ -77,6 +86,7 @@ export const discordAuthAtom = atom((get) => get(storeAtom)?.discordAuth); export const discordUrlsAtom = atom((get) => get(storeAtom)?.discordUrls); export const meAtom = atom((get) => get(storeAtom)?.me); export const leagueSummonerAtom = atom((get) => get(storeAtom)?.leagueSummoner); +export const regionAtom = atom((get) => get(storeAtom)?.locale?.region); export const LCUConnector = () => { const setFriends = useUpdateAtom(friendsAtom); diff --git a/src/features/Discord/AddSummonerButton.tsx b/src/features/Discord/AddSummonerButton.tsx index d9b0094..2a506da 100644 --- a/src/features/Discord/AddSummonerButton.tsx +++ b/src/features/Discord/AddSummonerButton.tsx @@ -7,7 +7,7 @@ export const AddSummonerButton = ({ guildId, channelId, summoners, - name, + guildName, isRestricted, }: DiscordGuild) => { const disclosure = useDisclosure(); @@ -27,7 +27,7 @@ export const AddSummonerButton = ({ removeSummonersMutation.mutate({ channelId: channelId, guildId: guildId, - summoners: summoners.map((summoner) => summoner.puuid), + summoners: summoners.map((summoner) => summoner.id), }); }} userSelect="none" @@ -41,7 +41,7 @@ export const AddSummonerButton = ({ queryClient.invalidateQueries("guilds") } ); const removeSummonersMutation = useRemoveSummonersMutation(); - + const region = useAtomValue(regionAtom); const [search, setSearch] = useState(""); const searchFriendlist = useSearchFriendlist(friendGroups, search); @@ -85,16 +86,20 @@ export const AddSummonerModal = ({ const onClick = () => { if (toRemove.length) { - console.log(toRemove); + console.log("toremove", toRemove); removeSummonersMutation.mutate({ channelId, guildId, - summoners: toRemove.map((summoner) => summoner.puuid), + summoners: toRemove.map((summoner) => summoner.id), }); } if (toAdd.length) { console.log(toAdd); - const payload = { channelId, guildId, summoners: toAdd }; + const payload = { + channelId, + guildId, + summoners: toAdd.map((summoner) => ({ ...summoner, region })), + }; addSummonersMutation.mutate(payload); } onClose(); diff --git a/src/features/Discord/DiscordGuildList.tsx b/src/features/Discord/DiscordGuildList.tsx index eea19c5..76f324a 100644 --- a/src/features/Discord/DiscordGuildList.tsx +++ b/src/features/Discord/DiscordGuildList.tsx @@ -27,7 +27,6 @@ export const useGuildsQuery = () => export const DiscordGuildList = (props: BoxProps) => { const guildsQuery = useGuildsQuery(); const { friends } = useFriendList(); - if (guildsQuery.isLoading) return (
@@ -42,6 +41,7 @@ export const DiscordGuildList = (props: BoxProps) => { ); const guilds = guildsQuery.data!; + console.log(guilds); return ( diff --git a/src/features/Discord/SummonerPanel.tsx b/src/features/Discord/SummonerPanel.tsx index 46a4caf..6467ddb 100644 --- a/src/features/Discord/SummonerPanel.tsx +++ b/src/features/Discord/SummonerPanel.tsx @@ -1,5 +1,5 @@ import { CloseIcon } from "@chakra-ui/icons"; -import { Box, Flex, Icon, Tooltip } from "@chakra-ui/react"; +import { Box, chakra, Flex, Icon, Tooltip } from "@chakra-ui/react"; import { FaUserFriends } from "react-icons/fa"; import { DiscordGuild } from "../../components/LCUConnector"; import { useRemoveSummonersMutation } from "./AddSummonerModal"; @@ -19,7 +19,10 @@ export const SummonerPanel = ({ return ( - {summoner.name} + + {summoner.name} + {summoner.region} + {isInFriendList && ( @@ -35,7 +38,7 @@ export const SummonerPanel = ({ removeSummonersMutation.mutate({ guildId, channelId, - summoners: [summoner.puuid], + summoners: [summoner.id], }) } />