diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..aed98a9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: cohstats diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..472abd9 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,6 @@ +name-template: "$RESOLVED_VERSION" +tag-template: "$RESOLVED_VERSION" +template: | + ## What’s Changed + + $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..776b587 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,34 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierrc.json b/.prettierrc.json index 0e23fe3..0c23202 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,6 @@ { "trailingComma": "es5", - "tabWidth": 4, + "tabWidth": 2, "semi": false, "singleQuote": false } \ No newline at end of file diff --git a/package.json b/package.json index 689d6d6..00bacf8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coh3-stats-desktop-app", "private": true, - "version": "1.2.1", + "version": "1.2.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 22f0cd1..7a8e570 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -421,7 +421,7 @@ dependencies = [ [[package]] name = "coh3-stats-desktop-app" -version = "1.2.1" +version = "1.2.2" dependencies = [ "log", "machine-uid", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d28893..0a3af38 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coh3-stats-desktop-app" -version = "1.2.1" +version = "1.2.2" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/src/parse_log_file.rs b/src-tauri/src/parse_log_file.rs index 213e99f..b63a9d3 100644 --- a/src-tauri/src/parse_log_file.rs +++ b/src-tauri/src/parse_log_file.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -use rev_lines::RevLines; use nom; use std::io::BufReader; +use std::io::BufRead; use std::fs::File; use log::{info}; @@ -62,7 +62,26 @@ pub struct LogFileData { #[tauri::command] pub fn parse_log_file_reverse(path: String) -> LogFileData { let file = File::open(path).unwrap(); - let rev_lines = RevLines::new(BufReader::new(file)).unwrap(); + let reader = BufReader::new(file); + + let mut string_array: Vec = Vec::new(); + + // Because some of the lines are not UTF-8, I needed to skip them while parsing + // this is less effective than using RevLines because we first load the whole log + // and than read it backwards. But I couldn't figure out how to fix it with revlines. + for result in reader.lines() { + match result { + Ok(line) => { + // If the conversion is successful, process the line + string_array.push(line); + } + Err(_) => { + // If the conversion fails, skip the line + // println!("Skipped non-UTF-8 line"); + } + } + } + let mut full_game = false; let mut game_running = true; let mut game_loading = false; @@ -77,8 +96,10 @@ pub fn parse_log_file_reverse(path: String) -> LogFileData { let mut player_name = "".to_string(); let mut player_steam_id = "".to_string(); let mut language_code = "".to_string(); + // Read log file in reverse order line by line - for line in rev_lines { + for line in string_array.iter().rev() { + // Is the line when the game is being closed correctly if nom::bytes::complete::tag::<&str, &str, ()>("Application closed")(line.as_str()).is_ok() { game_running = false; @@ -190,12 +211,12 @@ pub fn parse_log_file_reverse(path: String) -> LogFileData { } } - // Is the line that logs the playing players name + // Is the line that logs the playing players name } else if let Ok((steam_name, _)) = get_game_player_name(tail) { player_name = steam_name.to_string(); break; - // Is the line that logs the games language + // Is the line that logs the games language } else if let Ok((game_language, _)) = get_game_language(tail) { language_code = game_language.to_string(); } @@ -203,7 +224,7 @@ pub fn parse_log_file_reverse(path: String) -> LogFileData { if let Ok((duration_str, _)) = get_game_over(tail) { if !full_game { if let Ok(duration) = duration_str.parse::() { - game_duration = duration/8; + game_duration = duration / 8; //println!("Game Duration {}s", duration/8); } game_ended = true; @@ -211,12 +232,17 @@ pub fn parse_log_file_reverse(path: String) -> LogFileData { } } } - } + } + + } + let game_state = determine_game_state(game_running, game_ended, game_loading, game_started); let left_team = get_team_data(left); let right_team = get_team_data(right); + info!("Log file parsed: Found {} players", left_team.players.len() + right_team.players.len()); + LogFileData { game_state, game_type: determine_game_type(&left_team, &right_team), diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c194f03..e714195 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "Coh3 Stats Desktop App", - "version": "1.2.1" + "version": "1.2.2" }, "tauri": { "allowlist": { diff --git a/src/About.tsx b/src/About.tsx index 321fee1..b00c3a4 100644 --- a/src/About.tsx +++ b/src/About.tsx @@ -2,90 +2,97 @@ import { getVersion } from "@tauri-apps/api/app" import { open } from "@tauri-apps/api/shell" import { useState, useEffect } from "react" import { - Navbar, - AppShell, - Stack, - Title, - Code, - Text, - Group, - Anchor, - Divider, - Button, - Space, - Box, - Grid, + Navbar, + AppShell, + Stack, + Title, + Code, + Text, + Group, + Anchor, + Divider, + Button, + Space, + Box, + Grid, } from "@mantine/core" import logoBig from "./assets/logo/Square310x310Logo.png" import events from "./mixpanel/mixpanel" export const About: React.FC = () => { - const [appVersion, setAppVersion] = useState() - useEffect(() => { - getVersion().then((version) => setAppVersion(version)) - events.open_about() - }, []) + const [appVersion, setAppVersion] = useState() + useEffect(() => { + getVersion().then((version) => setAppVersion(version)) + events.open_about() + }, []) - return ( - <> - - About the App - - - - - Coh3Stats Logo - - - - Version - {appVersion} - - - Visit our website{" "} - open("https://coh3stats.com/")}> - coh3stats.com - - . - - - Want to help?{" "} - - open( - "https://github.com/cohstats/coh3-stats-desktop-app/issues" - ) - } - > - Report a bug - - ,{" "} - open("https://coh3stats.com/about")} - > - make a donation - {" "} - or{" "} - - open("https://discord.gg/jRrnwqMfkr") - } - > - join our discord and get involved! - - - - - - - ) + return ( + <> + + About the App + + + + + Coh3Stats Logo + + + + Version + {appVersion} + + + Visit our website{" "} + open("https://coh3stats.com/")}> + coh3stats.com + + . + + + Want to help?{" "} + + open( + "https://github.com/cohstats/coh3-stats-desktop-app/issues" + ) + } + > + Report a bug + + ,{" "} + open("https://coh3stats.com/about")}> + make a donation + {" "} + or{" "} + open("https://discord.gg/jRrnwqMfkr")}> + join our discord and get involved! + + + Reporting a bug + + In case of issues please, please try to report them in Discord + sections bugs-and-questions with as much details as possible. +
+ Also try to provide the warnings.log file from: +
{" "} + + {" "} + C:\Users\Username\Documents\My Games\Company of Heroes + 3\warnings.log + +
+ +
+
+ + ) } diff --git a/src/Game.tsx b/src/Game.tsx index 00adb7a..3e6a278 100644 --- a/src/Game.tsx +++ b/src/Game.tsx @@ -4,68 +4,60 @@ import { PlayerCard } from "./components/PlayerCard" import { useLogFilePath } from "./game-data-provider/configValues" export const Game: React.FC = () => { - const gameData = useGameData() - const logFilePath = useLogFilePath() - return ( + const gameData = useGameData() + const logFilePath = useLogFilePath() + return ( + <> + {gameData ? ( + + Game State: {gameData.gameData.state} + + ) : null} + {logFilePath !== undefined ? ( <> - {gameData ? ( - - Game State: {gameData.gameData.state} - - ) : null} - {logFilePath !== undefined ? ( - <> - {gameData && gameData.gameData.map.length > 0 ? ( - <> - - - {gameData.gameData.left.players.map( - (player, index) => ( - - ) - )} - - - - VS - - - - {gameData.gameData.right.players.map( - (player, index) => ( - - ) - )} - - - - ) : ( - - - <Loader mr="md" /> - Waiting for a game - - - )} - - ) : ( - - - <Loader mr="md" /> - Waiting for logfile - - - )} + {gameData && gameData.gameData.map.length > 0 ? ( + <> + + + {gameData.gameData.left.players.map((player, index) => ( + + ))} + + + + VS + + + + {gameData.gameData.right.players.map((player, index) => ( + + ))} + + + + ) : ( + + + <Loader mr="md" /> + Waiting for a game + + + )} - ) + ) : ( + + + <Loader mr="md" /> + Waiting for logfile + + + )} + + ) } diff --git a/src/Providers.tsx b/src/Providers.tsx index 5ef39cc..f5787e1 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -1,39 +1,39 @@ import { - MantineProvider, - ColorScheme, - ColorSchemeProvider, + MantineProvider, + ColorScheme, + ColorSchemeProvider, } from "@mantine/core" import { useLocalStorage } from "@mantine/hooks" import { useState } from "react" import { GameDataProvider } from "./game-data-provider/GameDataProvider" interface ProvidersProps { - children?: React.ReactNode + children?: React.ReactNode } export const Providers: React.FC = ({ children }) => { - const [colorScheme, setColorScheme] = useLocalStorage({ - key: "mantine-color-scheme", - defaultValue: "dark", - getInitialValueInEffect: true, - }) + const [colorScheme, setColorScheme] = useLocalStorage({ + key: "mantine-color-scheme", + defaultValue: "dark", + getInitialValueInEffect: true, + }) - const toggleColorScheme = (value?: ColorScheme) => - setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")) - return ( - <> - - - {children} - - - - ) + const toggleColorScheme = (value?: ColorScheme) => + setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")) + return ( + <> + + + {children} + + + + ) } diff --git a/src/Root.tsx b/src/Root.tsx index ba6af60..58b6ae5 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -2,11 +2,11 @@ import { Outlet } from "react-router-dom" import { WindowTitleBar } from "./WindowTitleBar" export const Root: React.FC = () => { - return ( - <> - - - - - ) + return ( + <> + + + + + ) } diff --git a/src/Router.tsx b/src/Router.tsx index 68c55d2..a20f744 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -5,30 +5,30 @@ import { Root } from "./Root" import { Settings } from "./Settings" export enum Routes { - GAME = "/", - SETTINGS = "/settings", - ABOUT = "/about", + GAME = "/", + SETTINGS = "/settings", + ABOUT = "/about", } const router = createBrowserRouter([ - { - path: "/", - element: , - children: [ - { - path: Routes.GAME, - element: , - }, - { - path: Routes.SETTINGS, - element: , - }, - { - path: Routes.ABOUT, - element: , - }, - ], - }, + { + path: "/", + element: , + children: [ + { + path: Routes.GAME, + element: , + }, + { + path: Routes.SETTINGS, + element: , + }, + { + path: Routes.ABOUT, + element: , + }, + ], + }, ]) export const Router: React.FC = () => diff --git a/src/Settings.tsx b/src/Settings.tsx index aa1602a..8f67ae0 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -1,18 +1,18 @@ import { ColorSchemeToggle } from "coh-stats-components" import { - Box, - Group, - Stack, - Divider, - Input, - ActionIcon, - Text, - List, - Button, - Tooltip, - Checkbox, - Slider, - Anchor, + Box, + Group, + Stack, + Divider, + Input, + ActionIcon, + Text, + List, + Button, + Tooltip, + Checkbox, + Slider, + Anchor, } from "@mantine/core" import { appDataDir } from "@tauri-apps/api/path" import { writeText } from "@tauri-apps/api/clipboard" @@ -22,245 +22,212 @@ import { open } from "@tauri-apps/api/dialog" import { open as openLink } from "@tauri-apps/api/shell" import { useLogFilePath } from "./game-data-provider/configValues" import { - usePlaySound, - usePlaySoundVolume, + usePlaySound, + usePlaySoundVolume, } from "./game-found-sound/configValues" import { - useShowFlagsOverlay, - useAlwaysShowOverlay, + useShowFlagsOverlay, + useAlwaysShowOverlay, } from "./streamer-overlay/configValues" import { playSound as playSoundFunc } from "./game-found-sound/playSound" import events from "./mixpanel/mixpanel" import { useGameData } from "./game-data-provider/GameDataProvider" export const Settings: React.FC = () => { - const gameData = useGameData() - const [logFilePath, setLogFilePath] = useLogFilePath() - const [playSound, setPlaySound] = usePlaySound() - const [playSoundVolume, setPlaySoundVolume] = usePlaySoundVolume() - const [showFlagsOverlay, setShowFlagsOverlay] = useShowFlagsOverlay() - const [alwaysShowOverlay, setAlwaysShowOverlay] = useAlwaysShowOverlay() - const [appDataPath, setAppDataPath] = useState("") + const gameData = useGameData() + const [logFilePath, setLogFilePath] = useLogFilePath() + const [playSound, setPlaySound] = usePlaySound() + const [playSoundVolume, setPlaySoundVolume] = usePlaySoundVolume() + const [showFlagsOverlay, setShowFlagsOverlay] = useShowFlagsOverlay() + const [alwaysShowOverlay, setAlwaysShowOverlay] = useAlwaysShowOverlay() + const [appDataPath, setAppDataPath] = useState("") - useEffect(() => { - events.open_settings() - }, []) + useEffect(() => { + events.open_settings() + }, []) - useEffect(() => { - const getAppDataPath = async () => { - const path = await appDataDir() - setAppDataPath(path) - } - if (appDataPath === "") { - getAppDataPath() - } - }, [appDataPath]) + useEffect(() => { + const getAppDataPath = async () => { + const path = await appDataDir() + setAppDataPath(path) + } + if (appDataPath === "") { + getAppDataPath() + } + }, [appDataPath]) - const openDialog = async () => { - const selected = await open({ - title: "Select Coh3 warnings.log file", - multiple: false, - directory: false, - defaultPath: logFilePath, - filters: [ - { - name: "Logfile", - extensions: ["log"], - }, - ], - }) - if (selected !== null) { - events.settings_changed("logFilePath", selected as string) - setLogFilePath(selected as string) - } + const openDialog = async () => { + const selected = await open({ + title: "Select Coh3 warnings.log file", + multiple: false, + directory: false, + defaultPath: logFilePath, + filters: [ + { + name: "Logfile", + extensions: ["log"], + }, + ], + }) + if (selected !== null) { + events.settings_changed("logFilePath", selected as string) + setLogFilePath(selected as string) } + } - return ( - <> - - - -
Color Theme:
-
- -
-
- -
Path to warnings.log:
-
- - - - - - - - {logFilePath !== undefined ? ( - - ) : ( - - )} - - - -
-
- -
Play sound on match found:
-
- - { - events.settings_changed( - "play_sound", - `${event.currentTarget.checked}` - ) - setPlaySound( - event.currentTarget.checked - ) - }} - /> - Volume: - {playSoundVolume.toFixed(1)} - ) : null - } - value={playSoundVolume} - onChange={setPlaySoundVolume} - onChangeEnd={(value) => { - events.settings_changed( - "play_sound_volume", - value - ) - }} - /> - - - - -
-
- - OBS Streamer Overlay: - -
Only show stats when loading / ingame:
-
- { - events.settings_changed( - "alwaysShowOverlay", - `${!event.currentTarget.checked}` - ) - setAlwaysShowOverlay( - !event.currentTarget.checked - ) - if (gameData) { - gameData.reloadLogFile() - } - }} - /> -
-
- -
Show flags:
-
- { - events.settings_changed( - "showFlagsOverlay", - `${event.currentTarget.checked}` - ) - setShowFlagsOverlay( - event.currentTarget.checked - ) - if (gameData) { - gameData.reloadLogFile() - } - }} - /> -
-
-
- - Follow the Setup instructions{" "} - - openLink( - "https://github.com/cohstats/coh3-stats-desktop-app#setup-obs-streamer-overlay" - ) - } - > - Here - - - - Path to streamerOverlay.html: - - - { - writeText(appDataPath) - }} - > - - - - -
-
-
- - ) + return ( + <> + + + +
Color Theme:
+
+ +
+
+ +
Path to warnings.log:
+
+ + + + + + + + {logFilePath !== undefined ? ( + + ) : ( + + )} + + + +
+
+ +
Play sound on match found:
+
+ + { + events.settings_changed( + "play_sound", + `${event.currentTarget.checked}` + ) + setPlaySound(event.currentTarget.checked) + }} + /> + Volume: + {playSoundVolume.toFixed(1)} : null + } + value={playSoundVolume} + onChange={setPlaySoundVolume} + onChangeEnd={(value) => { + events.settings_changed("play_sound_volume", value) + }} + /> + + + + +
+
+ + OBS Streamer Overlay: + +
Only show stats when loading / ingame:
+
+ { + events.settings_changed( + "alwaysShowOverlay", + `${!event.currentTarget.checked}` + ) + setAlwaysShowOverlay(!event.currentTarget.checked) + if (gameData) { + gameData.reloadLogFile() + } + }} + /> +
+
+ +
Show flags:
+
+ { + events.settings_changed( + "showFlagsOverlay", + `${event.currentTarget.checked}` + ) + setShowFlagsOverlay(event.currentTarget.checked) + if (gameData) { + gameData.reloadLogFile() + } + }} + /> +
+
+
+ + Follow the Setup instructions{" "} + + openLink( + "https://github.com/cohstats/coh3-stats-desktop-app#setup-obs-streamer-overlay" + ) + } + > + Here + + + + Path to streamerOverlay.html: + + + { + writeText(appDataPath) + }} + > + + + + +
+
+
+ + ) } diff --git a/src/WindowTitleBar.tsx b/src/WindowTitleBar.tsx index eaa595d..110ecbd 100644 --- a/src/WindowTitleBar.tsx +++ b/src/WindowTitleBar.tsx @@ -5,134 +5,127 @@ import logo from "./assets/logo/32x32.png" import { Routes } from "./Router" export interface WindowTitleBarProps { - children?: React.ReactNode + children?: React.ReactNode } const useStyles = createStyles((theme) => ({ - wrapper: { - display: "flex", - flexFlow: "column", - height: "100vh", - }, - header: { - backgroundColor: - theme.colorScheme === "dark" - ? theme.fn.darken(theme.colors.blue[9], 0.9) - : theme.fn.lighten(theme.colors.gray[3], 0.7), - flex: "0 1 auto", - }, - children: { - flex: "1 1 auto", - overflowY: "auto", - }, - link: { - display: "block", - lineHeight: 1, - padding: "10px 8px", - borderRadius: 0, - height: 35, - textDecoration: "none", - color: theme.colorScheme === "dark" ? theme.white : theme.black, - fontSize: theme.fontSizes.sm, - fontWeight: 500, - userSelect: "none", - cursor: "pointer", + wrapper: { + display: "flex", + flexFlow: "column", + height: "100vh", + }, + header: { + backgroundColor: + theme.colorScheme === "dark" + ? theme.fn.darken(theme.colors.blue[9], 0.9) + : theme.fn.lighten(theme.colors.gray[3], 0.7), + flex: "0 1 auto", + }, + children: { + flex: "1 1 auto", + overflowY: "auto", + }, + link: { + display: "block", + lineHeight: 1, + padding: "10px 8px", + borderRadius: 0, + height: 35, + textDecoration: "none", + color: theme.colorScheme === "dark" ? theme.white : theme.black, + fontSize: theme.fontSizes.sm, + fontWeight: 500, + userSelect: "none", + cursor: "pointer", - "&:hover": { - backgroundColor: - theme.colorScheme === "dark" - ? theme.fn.darken(theme.colors.blue[9], 0.7) - : theme.fn.lighten(theme.colors.gray[5], 0.7), - }, - }, - selectedLink: { - backgroundColor: - theme.colorScheme === "dark" - ? theme.fn.darken(theme.colors.blue[9], 0.7) - : theme.fn.lighten(theme.colors.blue[3], 0.7), - }, - windowButton: { - padding: "10px 12px", + "&:hover": { + backgroundColor: + theme.colorScheme === "dark" + ? theme.fn.darken(theme.colors.blue[9], 0.7) + : theme.fn.lighten(theme.colors.gray[5], 0.7), }, - closeButton: { - padding: "10px 14px", - "&:hover": { - backgroundColor: - theme.colorScheme === "dark" - ? theme.colors.red[8] - : theme.colors.red[7], - }, + }, + selectedLink: { + backgroundColor: + theme.colorScheme === "dark" + ? theme.fn.darken(theme.colors.blue[9], 0.7) + : theme.fn.lighten(theme.colors.blue[3], 0.7), + }, + windowButton: { + padding: "10px 12px", + }, + closeButton: { + padding: "10px 14px", + "&:hover": { + backgroundColor: + theme.colorScheme === "dark" + ? theme.colors.red[8] + : theme.colors.red[7], }, + }, })) export const WindowTitleBar: React.FC = ({ children }) => { - const { classes, cx } = useStyles() - const location = useLocation() - return ( - - ) + X + + + + + {children} + + ) } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..9f94138 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,39 @@ +import React from "react" + +class ErrorBoundary extends React.Component { + constructor(props: any) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: any) { + // Update state so the next render will show the fallback UI. + return { hasError: true } + } + + componentDidCatch(error: any, errorInfo: any) { + // You can also log the error to an error reporting service + // logErrorToMyService(error, errorInfo); + } + + render() { + // @ts-ignore + if (this.state.hasError) { + // You can render any custom fallback UI + return ( +
+ There was an error rendering the component. Please report this problem + in our Discord with as many details as possible. +
+ Try to provide a warnings.log C:\Users\Username\Documents\My + Games\Company of Heroes 3\warnings.log +
+ ) + } + + // @ts-ignore + return this.props.children + } +} + +export { ErrorBoundary } diff --git a/src/components/PlayerCard.tsx b/src/components/PlayerCard.tsx index 454e155..df77a58 100644 --- a/src/components/PlayerCard.tsx +++ b/src/components/PlayerCard.tsx @@ -1,15 +1,15 @@ import { - Anchor, - ColorSwatch, - Group, - Paper, - Stack, - Text, - Title, - Image, - Tooltip, - Grid, - Col, + Anchor, + ColorSwatch, + Group, + Paper, + Stack, + Text, + Title, + Image, + Tooltip, + Grid, + Col, } from "@mantine/core" import React from "react" import { FullPlayerData } from "../game-data-provider/GameData" @@ -24,74 +24,66 @@ import { open } from "@tauri-apps/api/shell" export interface PlayerCardProps extends FullPlayerData {} export const PlayerCard: React.FC = ({ - rank, - relicID, - name, - faction, - rating, - streak, - wins, - losses, - country, - color, - ai, - self, + rank, + relicID, + name, + faction, + rating, + streak, + wins, + losses, + country, + color, + ai, + self, }) => { - return ( - <> - - - - - {faction} - - - - - - {!ai ? ( - {country} - ) : null} + return ( + <> + + + + + {faction} + + + + + + {!ai ? ( + {country} + ) : null} - - open( - "https://coh3stats.com/players/" + - relicID - ) - } - > - <Anchor>{name}</Anchor>{" "} - {self ? <>(You)</> : null} - - {/**/} - + + open("https://coh3stats.com/players/" + relicID) + } + > + <Anchor>{name}</Anchor> {self ? <>(You)</> : null} + + {/**/} + - - - - - - - - - - - - - - ) + + + + + + + + + + + + + + ) } diff --git a/src/components/PlayerELO.tsx b/src/components/PlayerELO.tsx index d16477c..29ddd05 100644 --- a/src/components/PlayerELO.tsx +++ b/src/components/PlayerELO.tsx @@ -2,22 +2,22 @@ import { Text, Tooltip } from "@mantine/core" import React from "react" export interface PlayerELOProps { - rating: unknown + rating: unknown } export const PlayerELO: React.FC = ({ rating }) => { - let content = "-" - if (rating !== undefined && rating !== null) { - const ratingNumber = Number(rating) - if (!isNaN(ratingNumber) && ratingNumber > -1) { - content = "" + ratingNumber - } + let content = "-" + if (rating !== undefined && rating !== null) { + const ratingNumber = Number(rating) + if (!isNaN(ratingNumber) && ratingNumber > -1) { + content = "" + ratingNumber } - return ( - <> - - {content} - - - ) + } + return ( + <> + + {content} + + + ) } diff --git a/src/components/PlayerLosses.tsx b/src/components/PlayerLosses.tsx index fa234cc..e172b8d 100644 --- a/src/components/PlayerLosses.tsx +++ b/src/components/PlayerLosses.tsx @@ -2,24 +2,24 @@ import { MantineColor, Text, Tooltip } from "@mantine/core" import React from "react" export interface PlayerLossesProps { - losses: unknown + losses: unknown } export const PlayerLosses: React.FC = ({ losses }) => { - let content = "-" - let color: MantineColor | undefined - if (losses !== undefined && losses !== null) { - const lossesNumber = Number(losses) - if (!isNaN(lossesNumber) && lossesNumber >= 0) { - content = lossesNumber + "L" - color = "red" - } + let content = "-" + let color: MantineColor | undefined + if (losses !== undefined && losses !== null) { + const lossesNumber = Number(losses) + if (!isNaN(lossesNumber) && lossesNumber >= 0) { + content = lossesNumber + "L" + color = "red" } - return ( - <> - - {content} - - - ) + } + return ( + <> + + {content} + + + ) } diff --git a/src/components/PlayerRank.tsx b/src/components/PlayerRank.tsx index 65ce129..eb92e9c 100644 --- a/src/components/PlayerRank.tsx +++ b/src/components/PlayerRank.tsx @@ -2,22 +2,22 @@ import { Text, Tooltip } from "@mantine/core" import React from "react" export interface PlayerRankProps { - rank: unknown + rank: unknown } export const PlayerRank: React.FC = ({ rank }) => { - let content = "-" - if (rank !== undefined && rank !== null) { - const rankNumber = Number(rank) - if (!isNaN(rankNumber) && rankNumber > -1) { - content = "#" + rankNumber - } + let content = "-" + if (rank !== undefined && rank !== null) { + const rankNumber = Number(rank) + if (!isNaN(rankNumber) && rankNumber > -1) { + content = "#" + rankNumber } - return ( - <> - - {content} - - - ) + } + return ( + <> + + {content} + + + ) } diff --git a/src/components/PlayerStreak.tsx b/src/components/PlayerStreak.tsx index f9c5deb..64ff461 100644 --- a/src/components/PlayerStreak.tsx +++ b/src/components/PlayerStreak.tsx @@ -2,30 +2,30 @@ import { MantineColor, Text, Tooltip } from "@mantine/core" import React from "react" export interface PlayerStreakProps { - streak: unknown + streak: unknown } export const PlayerStreak: React.FC = ({ streak }) => { - let content = "-" - let color: MantineColor | undefined - if (streak !== undefined && streak !== null) { - const streakNumber = Number(streak) - if (!isNaN(streakNumber)) { - content = "" + streakNumber - if (streakNumber > 0) { - color = "green" - content = "+" + streakNumber - } - if (streakNumber < 0) { - color = "red" - } - } + let content = "-" + let color: MantineColor | undefined + if (streak !== undefined && streak !== null) { + const streakNumber = Number(streak) + if (!isNaN(streakNumber)) { + content = "" + streakNumber + if (streakNumber > 0) { + color = "green" + content = "+" + streakNumber + } + if (streakNumber < 0) { + color = "red" + } } - return ( - <> - - {content} - - - ) + } + return ( + <> + + {content} + + + ) } diff --git a/src/components/PlayerWinRatio.tsx b/src/components/PlayerWinRatio.tsx index 0d6abbe..4e698d9 100644 --- a/src/components/PlayerWinRatio.tsx +++ b/src/components/PlayerWinRatio.tsx @@ -2,40 +2,39 @@ import { Text, Tooltip } from "@mantine/core" import React from "react" export interface PlayerWinRatioProps { - wins: unknown - losses: unknown + wins: unknown + losses: unknown } export const PlayerWinRatio: React.FC = ({ - wins, - losses, + wins, + losses, }) => { - let content = "-" + let content = "-" + if ( + wins !== undefined && + wins !== null && + losses !== undefined && + losses !== null + ) { + const winsNumber = Number(wins) + const lossesNumber = Number(losses) if ( - wins !== undefined && - wins !== null && - losses !== undefined && - losses !== null + !isNaN(winsNumber) && + winsNumber >= 0 && + !isNaN(lossesNumber) && + lossesNumber >= 0 && + lossesNumber + winsNumber > 0 ) { - const winsNumber = Number(wins) - const lossesNumber = Number(losses) - if ( - !isNaN(winsNumber) && - winsNumber >= 0 && - !isNaN(lossesNumber) && - lossesNumber >= 0 && - lossesNumber + winsNumber > 0 - ) { - content = - ((winsNumber / (winsNumber + lossesNumber)) * 100).toFixed(0) + - "%" - } + content = + ((winsNumber / (winsNumber + lossesNumber)) * 100).toFixed(0) + "%" } - return ( - <> - - {content} - - - ) + } + return ( + <> + + {content} + + + ) } diff --git a/src/components/PlayerWins.tsx b/src/components/PlayerWins.tsx index 3606934..f222f39 100644 --- a/src/components/PlayerWins.tsx +++ b/src/components/PlayerWins.tsx @@ -2,24 +2,24 @@ import { MantineColor, Text, Tooltip } from "@mantine/core" import React from "react" export interface PlayerWinsProps { - wins: unknown + wins: unknown } export const PlayerWins: React.FC = ({ wins }) => { - let content = "-" - let color: MantineColor | undefined - if (wins !== undefined && wins !== null) { - const winsNumber = Number(wins) - if (!isNaN(winsNumber) && winsNumber >= 0) { - content = winsNumber + "W" - color = "green" - } + let content = "-" + let color: MantineColor | undefined + if (wins !== undefined && wins !== null) { + const winsNumber = Number(wins) + if (!isNaN(winsNumber) && winsNumber >= 0) { + content = winsNumber + "W" + color = "green" } - return ( - <> - - {content} - - - ) + } + return ( + <> + + {content} + + + ) } diff --git a/src/config-store/configValueFactory.tsx b/src/config-store/configValueFactory.tsx index b6dd8b4..c103b0d 100644 --- a/src/config-store/configValueFactory.tsx +++ b/src/config-store/configValueFactory.tsx @@ -6,77 +6,77 @@ import { getStore } from "./store" const CONFIG_CHANGE_EVENT = new EventEmitter() export const configValueFactory = ( - key: string, - defaultValueFunc: () => Promise, - validatorFunc?: (value: T, store: Store, defaultValue: T) => Promise + key: string, + defaultValueFunc: () => Promise, + validatorFunc?: (value: T, store: Store, defaultValue: T) => Promise ) => { - const reactHook = () => { - const [value, setValue] = useState() - const valueInitializedRef = useRef(false) - - useEffect(() => { - const init = async () => { - const store = await getStore() - const storeValue = await store.get(key) - const defaultValue = await defaultValueFunc() - let validatedValue = defaultValue - if (validatorFunc !== undefined) { - if (storeValue === null) { - validatedValue = await validatorFunc( - defaultValue, - store, - defaultValue - ) - } else { - validatedValue = await validatorFunc( - storeValue, - store, - defaultValue - ) - } - } else if (storeValue !== null) { - validatedValue = storeValue - } - await store.set(key, validatedValue) - await store.save() - CONFIG_CHANGE_EVENT.emit(key, validatedValue) - valueInitializedRef.current = true - } - if (valueInitializedRef.current === false) { - init() - } - const onChange = (value: T) => { - setValue(value) - } - CONFIG_CHANGE_EVENT.on(key, onChange) - return () => { - CONFIG_CHANGE_EVENT.off(key, onChange) - } - }, []) - - const setValueExtern = async (value: T) => { - const store = await getStore() - let validatedValue = value - if (validatorFunc) { - const defaultValue = await defaultValueFunc() - validatedValue = await validatorFunc(value, store, defaultValue) - } - await store.set(key, validatedValue) - await store.save() - CONFIG_CHANGE_EVENT.emit(key, validatedValue) - } + const reactHook = () => { + const [value, setValue] = useState() + const valueInitializedRef = useRef(false) - return [value, setValueExtern] as const - } - const getter: () => Promise = async () => { + useEffect(() => { + const init = async () => { const store = await getStore() const storeValue = await store.get(key) - if (storeValue === null) { - return await defaultValueFunc() + const defaultValue = await defaultValueFunc() + let validatedValue = defaultValue + if (validatorFunc !== undefined) { + if (storeValue === null) { + validatedValue = await validatorFunc( + defaultValue, + store, + defaultValue + ) + } else { + validatedValue = await validatorFunc( + storeValue, + store, + defaultValue + ) + } + } else if (storeValue !== null) { + validatedValue = storeValue } - return storeValue + await store.set(key, validatedValue) + await store.save() + CONFIG_CHANGE_EVENT.emit(key, validatedValue) + valueInitializedRef.current = true + } + if (valueInitializedRef.current === false) { + init() + } + const onChange = (value: T) => { + setValue(value) + } + CONFIG_CHANGE_EVENT.on(key, onChange) + return () => { + CONFIG_CHANGE_EVENT.off(key, onChange) + } + }, []) + + const setValueExtern = async (value: T) => { + const store = await getStore() + let validatedValue = value + if (validatorFunc) { + const defaultValue = await defaultValueFunc() + validatedValue = await validatorFunc(value, store, defaultValue) + } + await store.set(key, validatedValue) + await store.save() + CONFIG_CHANGE_EVENT.emit(key, validatedValue) } - return [getter, reactHook] as const + + return [value, setValueExtern] as const + } + const getter: () => Promise = async () => { + const store = await getStore() + const storeValue = await store.get(key) + if (storeValue === null) { + return await defaultValueFunc() + } + return storeValue + } + return [getter, reactHook] as const } /*export const useConfigValue = ( diff --git a/src/config-store/store.tsx b/src/config-store/store.tsx index 7cfb522..f419372 100644 --- a/src/config-store/store.tsx +++ b/src/config-store/store.tsx @@ -4,14 +4,14 @@ import { Store } from "tauri-plugin-store-api" let CONFIG_StORE: Store | undefined export const getStore = async () => { - if (CONFIG_StORE === undefined) { - const appDataPath = await appDataDir() - CONFIG_StORE = new Store(appDataPath + "config.dat") - try { - await CONFIG_StORE.load() - } catch { - await CONFIG_StORE.save() - } + if (CONFIG_StORE === undefined) { + const appDataPath = await appDataDir() + CONFIG_StORE = new Store(appDataPath + "config.dat") + try { + await CONFIG_StORE.load() + } catch { + await CONFIG_StORE.save() } - return CONFIG_StORE + } + return CONFIG_StORE } diff --git a/src/game-data-provider/GameData.ts b/src/game-data-provider/GameData.ts index c27df03..4089b5d 100644 --- a/src/game-data-provider/GameData.ts +++ b/src/game-data-provider/GameData.ts @@ -9,86 +9,86 @@ export type GameType = "Classic" | "AI" | "Custom" export type TeamSide = "Axis" | "Allies" | "Mixed" export interface RawPlayerData { - ai: boolean - faction: logFileRaceType - relic_id: string - name: string - position: number - steam_id: string - rank: number + ai: boolean + faction: logFileRaceType + relic_id: string + name: string + position: number + steam_id: string + rank: number } export interface RawTeamData { - players: RawPlayerData[] - side: TeamSide + players: RawPlayerData[] + side: TeamSide } export interface RawGameData { - game_state: GameState - game_type: GameType - /** Timestamp in log file when the last game started. This timestamp represents the time since coh3 was launched! */ - timestamp: string - /** Duration in seconds */ - duration: number - map: string - win_condition: string - left: RawTeamData - right: RawTeamData - player_name: string - player_steam_id: string - language_code: string + game_state: GameState + game_type: GameType + /** Timestamp in log file when the last game started. This timestamp represents the time since coh3 was launched! */ + timestamp: string + /** Duration in seconds */ + duration: number + map: string + win_condition: string + left: RawTeamData + right: RawTeamData + player_name: string + player_steam_id: string + language_code: string } export interface FullPlayerData { - ai: boolean - self: boolean - faction: raceType - relicID: string - name: string - position: number - steamID?: string - country?: string - level?: number - xp?: number - disputes?: number - drops?: number - lastMatchDate?: number - losses?: number - rank?: number - rankLevel?: number - rankTotal?: number - rating?: number - regionRank?: number - regionRankTotal?: number - streak?: number - wins?: number - color: MantineColor + ai: boolean + self: boolean + faction: raceType + relicID: string + name: string + position: number + steamID?: string + country?: string + level?: number + xp?: number + disputes?: number + drops?: number + lastMatchDate?: number + losses?: number + rank?: number + rankLevel?: number + rankTotal?: number + rating?: number + regionRank?: number + regionRankTotal?: number + streak?: number + wins?: number + color: MantineColor } export interface FullTeamData { - players: FullPlayerData[] - side: TeamSide - //averageRating: number - //averageRank: number - //averageWinRatio: number + players: FullPlayerData[] + side: TeamSide + //averageRating: number + //averageRank: number + //averageWinRatio: number } export interface FullGameData { - uniqueID: string - state: GameState - type: GameType - timestamp: string - duration: number - map: string - winCondition: string - left: FullTeamData - right: FullTeamData - language_code: string + uniqueID: string + state: GameState + type: GameType + timestamp: string + duration: number + map: string + winCondition: string + left: FullTeamData + right: FullTeamData + language_code: string } export interface LogFileFoundGameData { - gameData: FullGameData - reloadLogFile: () => void + gameData: FullGameData + reloadLogFile: () => void } export type GameData = LogFileFoundGameData | undefined diff --git a/src/game-data-provider/GameDataProvider.tsx b/src/game-data-provider/GameDataProvider.tsx index fd17901..f8def3c 100644 --- a/src/game-data-provider/GameDataProvider.tsx +++ b/src/game-data-provider/GameDataProvider.tsx @@ -7,28 +7,28 @@ const GameDataContext = React.createContext(undefined) export const useGameData = () => useContext(GameDataContext) export interface GameDataProviderProps { - children?: React.ReactNode + children?: React.ReactNode } export const GameDataProvider: React.FC = ({ - children, + children, }) => { - const { gameData, reloadLogFile } = useFullGameData() - const [logFilePath] = useLogFilePath() - return ( - <> - - {children} - - - ) + const { gameData, reloadLogFile } = useFullGameData() + const [logFilePath] = useLogFilePath() + return ( + <> + + {children} + + + ) } diff --git a/src/game-data-provider/configValues.tsx b/src/game-data-provider/configValues.tsx index 263d9a1..17e928d 100644 --- a/src/game-data-provider/configValues.tsx +++ b/src/game-data-provider/configValues.tsx @@ -2,24 +2,24 @@ import { configValueFactory } from "../config-store/configValueFactory" import { invoke } from "@tauri-apps/api/tauri" const [getLogFilePath, useLogFilePath] = configValueFactory( - "logFilePath", - async () => (await invoke("get_default_log_file_path")) as string, - async (value, store, defaultValue) => { - const logFileExists = (await invoke("check_log_file_exists", { - path: value, - })) as boolean - if (logFileExists) { - return value - } - const defaultLogFileExists = (await invoke("check_log_file_exists", { - path: defaultValue, - })) as boolean - if (defaultLogFileExists) { - return defaultValue - } - - return undefined + "logFilePath", + async () => (await invoke("get_default_log_file_path")) as string, + async (value, store, defaultValue) => { + const logFileExists = (await invoke("check_log_file_exists", { + path: value, + })) as boolean + if (logFileExists) { + return value + } + const defaultLogFileExists = (await invoke("check_log_file_exists", { + path: defaultValue, + })) as boolean + if (defaultLogFileExists) { + return defaultValue } + + return undefined + } ) export { getLogFilePath, useLogFilePath } diff --git a/src/game-data-provider/useFullGameData.tsx b/src/game-data-provider/useFullGameData.tsx index cd87fd5..e56314e 100644 --- a/src/game-data-provider/useFullGameData.tsx +++ b/src/game-data-provider/useFullGameData.tsx @@ -1,19 +1,19 @@ import { useCallback, useEffect, useRef, useState } from "react" import { RawLaddersObject } from "coh3-data-types-library" import { - FullGameData, - FullPlayerData, - GameState, - RawGameData, - RawTeamData, + FullGameData, + FullPlayerData, + GameState, + RawGameData, + RawTeamData, } from "./GameData" import { useRawGameData } from "./useRawGameData" import { fetch } from "@tauri-apps/api/http" import { - BASE_RELIC_API_URL, - leaderboardsIDAsObject, - leaderBoardType, - logFileRaceTypeToRaceType, + BASE_RELIC_API_URL, + leaderboardsIDAsObject, + leaderBoardType, + logFileRaceTypeToRaceType, } from "coh3-data-types-library" import { MantineColor } from "@mantine/core" import { renderStreamerHTML } from "../streamer-overlay/renderStreamerOverlay" @@ -22,207 +22,190 @@ import { playSound as playSoundFunc } from "../game-found-sound/playSound" import { getPlaySound } from "../game-found-sound/configValues" const PLAYER_COLOR_OBJECT: { left: MantineColor[]; right: MantineColor[] } = { - left: ["blue", "blue", "blue", "blue"], - right: ["pink", "green", "red", "purple"], + left: ["blue", "blue", "blue", "blue"], + right: ["pink", "green", "red", "purple"], } export const useFullGameData = () => { - const { rawGameData } = useRawGameData() - const [logFilePath] = useLogFilePath() - const lastGameUniqueKeyRef = useRef("") - const lastGameStateRef = useRef() - const [gameData, setGameData] = useState() + const { rawGameData } = useRawGameData() + const [logFilePath] = useLogFilePath() + const lastGameUniqueKeyRef = useRef("") + const lastGameStateRef = useRef() + const [gameData, setGameData] = useState() - const generateUniqueGameKey = useCallback((rawGameData: RawGameData) => { - return ( - rawGameData.timestamp + - rawGameData.map + - rawGameData.win_condition + - rawGameData.left.players - .concat(rawGameData.right.players) - .map((player) => player.relic_id) - .join() - ) - }, []) + const generateUniqueGameKey = useCallback((rawGameData: RawGameData) => { + return ( + rawGameData.timestamp + + rawGameData.map + + rawGameData.win_condition + + rawGameData.left.players + .concat(rawGameData.right.players) + .map((player) => player.relic_id) + .join() + ) + }, []) - useEffect(() => { - const refineSide = async ( - side: RawTeamData, - left: boolean, - rawGameData: RawGameData - ) => { - const gameMode = (side.players.length + - "v" + - side.players.length) as leaderBoardType - const onlyRealPlayers = side.players.filter((player) => !player.ai) - let responses = await Promise.all( - onlyRealPlayers.map((player) => - fetch( - BASE_RELIC_API_URL + - "/community/leaderboard/getpersonalstat?profile_ids=[" + - player.relic_id + - "]&title=coh3", - { - method: "GET", - } - ) - ) - ) - console.log("FETCH DATA") - let mergedResponses = responses.map((response, index) => ({ - response, - relicID: onlyRealPlayers[index].relic_id, - faction: - logFileRaceTypeToRaceType[onlyRealPlayers[index].faction], - })) - let refinedPlayerData = side.players.map( - (player, index): FullPlayerData => ({ - ai: player.ai, - faction: logFileRaceTypeToRaceType[player.faction], - relicID: player.relic_id, - name: player.name, - position: player.position, - color: left - ? PLAYER_COLOR_OBJECT.left[index] - : PLAYER_COLOR_OBJECT.right[index], - self: player.name === rawGameData.player_name, - }) - ) - mergedResponses.forEach((response) => { - const data = response.response.data as RawLaddersObject - if (data.result && data.result.message === "SUCCESS") { - const refinedPlayerIndex = refinedPlayerData.findIndex( - (player) => player.relicID === response.relicID - ) - if (refinedPlayerIndex === -1) { - return - } - const member = data.statGroups[0].members.find( - (member) => member.profile_id + "" === response.relicID - ) - if (member) { - refinedPlayerData[refinedPlayerIndex].country = - member.country - refinedPlayerData[refinedPlayerIndex].steamID = - member.name.split("/").at(-1) - refinedPlayerData[refinedPlayerIndex].level = - member.level - refinedPlayerData[refinedPlayerIndex].xp = member.xp - } - const leaderboardID = - leaderboardsIDAsObject[gameMode][response.faction] - const leaderboard = data.leaderboardStats.find( - (leaderboard) => - leaderboard.leaderboard_id === leaderboardID - ) - if (leaderboard) { - refinedPlayerData[refinedPlayerIndex].disputes = - leaderboard.disputes - refinedPlayerData[refinedPlayerIndex].drops = - leaderboard.drops - refinedPlayerData[refinedPlayerIndex].lastMatchDate = - leaderboard.lastmatchdate - refinedPlayerData[refinedPlayerIndex].losses = - leaderboard.losses - refinedPlayerData[refinedPlayerIndex].rank = - leaderboard.rank - refinedPlayerData[refinedPlayerIndex].rankLevel = - leaderboard.ranklevel - refinedPlayerData[refinedPlayerIndex].rankTotal = - leaderboard.ranktotal - refinedPlayerData[refinedPlayerIndex].rating = - leaderboard.rating - refinedPlayerData[refinedPlayerIndex].regionRank = - leaderboard.regionrank - refinedPlayerData[refinedPlayerIndex].regionRankTotal = - leaderboard.regionranktotal - refinedPlayerData[refinedPlayerIndex].streak = - leaderboard.streak - refinedPlayerData[refinedPlayerIndex].wins = - leaderboard.wins - } - } - }) - return refinedPlayerData - } - const swapTeamsBasedOnGamePlayer = ( - teams: [FullPlayerData[], FullPlayerData[]], - rawGameData: RawGameData - ) => { - if ( - teams[1].find( - (player) => player.name === rawGameData.player_name - ) - ) { - return [teams[1], teams[0]] + useEffect(() => { + const refineSide = async ( + side: RawTeamData, + left: boolean, + rawGameData: RawGameData + ) => { + const gameMode = (side.players.length + + "v" + + side.players.length) as leaderBoardType + const onlyRealPlayers = side.players.filter((player) => !player.ai) + let responses = await Promise.all( + onlyRealPlayers.map((player) => + fetch( + BASE_RELIC_API_URL + + "/community/leaderboard/getpersonalstat?profile_ids=[" + + player.relic_id + + "]&title=coh3", + { + method: "GET", } - return [teams[0], teams[1]] + ) + ) + ) + console.log("FETCH DATA") + let mergedResponses = responses.map((response, index) => ({ + response, + relicID: onlyRealPlayers[index].relic_id, + faction: logFileRaceTypeToRaceType[onlyRealPlayers[index].faction], + })) + let refinedPlayerData = side.players.map( + (player, index): FullPlayerData => ({ + ai: player.ai, + faction: logFileRaceTypeToRaceType[player.faction], + relicID: player.relic_id, + name: player.name, + position: player.position, + color: left + ? PLAYER_COLOR_OBJECT.left[index] + : PLAYER_COLOR_OBJECT.right[index], + self: player.name === rawGameData.player_name, + }) + ) + mergedResponses.forEach((response) => { + const data = response.response.data as RawLaddersObject + if (data.result && data.result.message === "SUCCESS") { + const refinedPlayerIndex = refinedPlayerData.findIndex( + (player) => player.relicID === response.relicID + ) + if (refinedPlayerIndex === -1) { + return + } + const member = data.statGroups[0].members.find( + (member) => member.profile_id + "" === response.relicID + ) + if (member) { + refinedPlayerData[refinedPlayerIndex].country = member.country + refinedPlayerData[refinedPlayerIndex].steamID = member.name + .split("/") + .at(-1) + refinedPlayerData[refinedPlayerIndex].level = member.level + refinedPlayerData[refinedPlayerIndex].xp = member.xp + } + const leaderboardID = + leaderboardsIDAsObject[gameMode][response.faction] + const leaderboard = data.leaderboardStats.find( + (leaderboard) => leaderboard.leaderboard_id === leaderboardID + ) + if (leaderboard) { + refinedPlayerData[refinedPlayerIndex].disputes = + leaderboard.disputes + refinedPlayerData[refinedPlayerIndex].drops = leaderboard.drops + refinedPlayerData[refinedPlayerIndex].lastMatchDate = + leaderboard.lastmatchdate + refinedPlayerData[refinedPlayerIndex].losses = leaderboard.losses + refinedPlayerData[refinedPlayerIndex].rank = leaderboard.rank + refinedPlayerData[refinedPlayerIndex].rankLevel = + leaderboard.ranklevel + refinedPlayerData[refinedPlayerIndex].rankTotal = + leaderboard.ranktotal + refinedPlayerData[refinedPlayerIndex].rating = leaderboard.rating + refinedPlayerData[refinedPlayerIndex].regionRank = + leaderboard.regionrank + refinedPlayerData[refinedPlayerIndex].regionRankTotal = + leaderboard.regionranktotal + refinedPlayerData[refinedPlayerIndex].streak = leaderboard.streak + refinedPlayerData[refinedPlayerIndex].wins = leaderboard.wins + } } - const refineLogFileData = async (rawGameData: RawGameData) => { - const playSound = await getPlaySound() - if (playSound && rawGameData.game_state === "Loading") { - playSoundFunc() - } - try { - const [leftRefined, rightRefined] = swapTeamsBasedOnGamePlayer( - await Promise.all([ - refineSide(rawGameData.left, true, rawGameData), - refineSide(rawGameData.right, false, rawGameData), - ]), - rawGameData - ) - const newGameData: FullGameData = { - uniqueID: generateUniqueGameKey(rawGameData), - state: rawGameData.game_state, - type: rawGameData.game_type, - timestamp: rawGameData.timestamp, - duration: rawGameData.duration, - map: rawGameData.map, - winCondition: rawGameData.win_condition, - left: { - side: rawGameData.left.side, - players: leftRefined, - }, - right: { - side: rawGameData.right.side, - players: rightRefined, - }, - language_code: rawGameData.language_code, - } - renderStreamerHTML(newGameData) - setGameData(newGameData) - } catch (e: any) { - console.error(e) - } + }) + return refinedPlayerData + } + const swapTeamsBasedOnGamePlayer = ( + teams: [FullPlayerData[], FullPlayerData[]], + rawGameData: RawGameData + ) => { + if (teams[1].find((player) => player.name === rawGameData.player_name)) { + return [teams[1], teams[0]] + } + return [teams[0], teams[1]] + } + const refineLogFileData = async (rawGameData: RawGameData) => { + const playSound = await getPlaySound() + if (playSound && rawGameData.game_state === "Loading") { + playSoundFunc() + } + try { + const [leftRefined, rightRefined] = swapTeamsBasedOnGamePlayer( + await Promise.all([ + refineSide(rawGameData.left, true, rawGameData), + refineSide(rawGameData.right, false, rawGameData), + ]), + rawGameData + ) + const newGameData: FullGameData = { + uniqueID: generateUniqueGameKey(rawGameData), + state: rawGameData.game_state, + type: rawGameData.game_type, + timestamp: rawGameData.timestamp, + duration: rawGameData.duration, + map: rawGameData.map, + winCondition: rawGameData.win_condition, + left: { + side: rawGameData.left.side, + players: leftRefined, + }, + right: { + side: rawGameData.right.side, + players: rightRefined, + }, + language_code: rawGameData.language_code, } - // when raw data from log file changes check if its a new game with the generated unique key and refine data external api data - if (logFilePath !== undefined && rawGameData) { - if ( - lastGameUniqueKeyRef.current !== - generateUniqueGameKey(rawGameData) - ) { - refineLogFileData(rawGameData) - lastGameUniqueKeyRef.current = - generateUniqueGameKey(rawGameData) - } else if (lastGameStateRef.current !== rawGameData.game_state) { - if (gameData) { - lastGameStateRef.current = rawGameData.game_state - const newGameData = gameData - newGameData.state = rawGameData.game_state - renderStreamerHTML(newGameData) - setGameData(newGameData) - } - } + renderStreamerHTML(newGameData) + setGameData(newGameData) + } catch (e: any) { + console.error(e) + } + } + // when raw data from log file changes check if its a new game with the generated unique key and refine data external api data + if (logFilePath !== undefined && rawGameData) { + if (lastGameUniqueKeyRef.current !== generateUniqueGameKey(rawGameData)) { + refineLogFileData(rawGameData) + lastGameUniqueKeyRef.current = generateUniqueGameKey(rawGameData) + } else if (lastGameStateRef.current !== rawGameData.game_state) { + if (gameData) { + lastGameStateRef.current = rawGameData.game_state + const newGameData = gameData + newGameData.state = rawGameData.game_state + renderStreamerHTML(newGameData) + setGameData(newGameData) } - }, [logFilePath, rawGameData]) - - const reloadLogFile = () => { - lastGameUniqueKeyRef.current = "" + } } + }, [logFilePath, rawGameData]) - return { - rawGameData, - gameData, - reloadLogFile, - } + const reloadLogFile = () => { + lastGameUniqueKeyRef.current = "" + } + + return { + rawGameData, + gameData, + reloadLogFile, + } } diff --git a/src/game-data-provider/useRawGameData.tsx b/src/game-data-provider/useRawGameData.tsx index beb573f..dc46871 100644 --- a/src/game-data-provider/useRawGameData.tsx +++ b/src/game-data-provider/useRawGameData.tsx @@ -5,36 +5,36 @@ import { useLogFilePath } from "./configValues" /** This hook handles the collection of raw game data from the log file */ export const useRawGameData = () => { - const [logFilePath] = useLogFilePath() - const [rawGameData, setRawGameData] = useState() - const intervalRef = useRef() - const getLogFileData = async (path: string) => { - const data = (await invoke("parse_log_file_reverse", { - path, - })) as RawGameData - setRawGameData(data) - } + const [logFilePath] = useLogFilePath() + const [rawGameData, setRawGameData] = useState() + const intervalRef = useRef() + const getLogFileData = async (path: string) => { + const data = (await invoke("parse_log_file_reverse", { + path, + })) as RawGameData + setRawGameData(data) + } - const reloadLogFile = () => { - if (logFilePath !== undefined) { - getLogFileData(logFilePath) - } + const reloadLogFile = () => { + if (logFilePath !== undefined) { + getLogFileData(logFilePath) } - // when log file exists start watching the log file - useEffect(() => { + } + // when log file exists start watching the log file + useEffect(() => { + if (logFilePath !== undefined) { + if (intervalRef.current !== undefined) { + clearInterval(intervalRef.current) + } + intervalRef.current = setInterval(() => { if (logFilePath !== undefined) { - if (intervalRef.current !== undefined) { - clearInterval(intervalRef.current) - } - intervalRef.current = setInterval(() => { - if (logFilePath !== undefined) { - getLogFileData(logFilePath) - } - }, 2000) + getLogFileData(logFilePath) } - }, [logFilePath]) - return { - rawGameData, - reloadLogFile, + }, 2000) } + }, [logFilePath]) + return { + rawGameData, + reloadLogFile, + } } diff --git a/src/game-found-sound/configValues.tsx b/src/game-found-sound/configValues.tsx index a111583..1fb2c60 100644 --- a/src/game-found-sound/configValues.tsx +++ b/src/game-found-sound/configValues.tsx @@ -1,13 +1,13 @@ import { configValueFactory } from "../config-store/configValueFactory" const [getPlaySound, usePlaySound] = configValueFactory( - "playSound", - async () => false + "playSound", + async () => false ) const [getPlaySoundVolume, usePlaySoundVolume] = configValueFactory( - "playSoundVolume", - async () => 0.8 + "playSoundVolume", + async () => 0.8 ) export { getPlaySound, usePlaySound, getPlaySoundVolume, usePlaySoundVolume } diff --git a/src/game-found-sound/playSound.ts b/src/game-found-sound/playSound.ts index a0a0de5..fcaff2a 100644 --- a/src/game-found-sound/playSound.ts +++ b/src/game-found-sound/playSound.ts @@ -1,7 +1,7 @@ import { getPlaySoundVolume } from "./configValues" export const playSound = async () => { - const audio = new Audio("/hoorah.wav") - audio.volume = await getPlaySoundVolume() - audio.play() + const audio = new Audio("/hoorah.wav") + audio.volume = await getPlaySoundVolume() + audio.play() } diff --git a/src/main.tsx b/src/main.tsx index d1f66b9..242bcb3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,33 +14,33 @@ events.init() // make sure an html file exists renderStreamerHTML({ - uniqueID: "", - state: "Closed", - type: "Classic", - timestamp: "", - duration: 0, - map: "", - winCondition: "", - left: { - players: [], - side: "Mixed", - }, - right: { - players: [], - side: "Mixed", - }, - language_code: "", + uniqueID: "", + state: "Closed", + type: "Classic", + timestamp: "", + duration: 0, + map: "", + winCondition: "", + left: { + players: [], + side: "Mixed", + }, + right: { + players: [], + side: "Mixed", + }, + language_code: "", }) listen("single-instance", () => { - //appWindow.requestUserAttention(2) - //appWindow.setFocus() + //appWindow.requestUserAttention(2) + //appWindow.setFocus() }) ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - + + + + + ) diff --git a/src/mixpanel/mixpanel.ts b/src/mixpanel/mixpanel.ts index 57ef92a..0b8b125 100644 --- a/src/mixpanel/mixpanel.ts +++ b/src/mixpanel/mixpanel.ts @@ -7,35 +7,35 @@ mixpanel.init("bf92acb0810b9e7d4a49e63efc41433d") * The events for Mixpanel */ const events = { - init: async (): Promise => { - mixpanel.track("app_init", { - distinct_id: await getClientId(), - version: await getVersion(), - }) - }, - open_about: async (): Promise => { - mixpanel.track("open_about", { - distinct_id: await getClientId(), - version: await getVersion(), - }) - }, - open_settings: async (): Promise => { - mixpanel.track("open_settings", { - distinct_id: await getClientId(), - version: await getVersion(), - }) - }, - settings_changed: async ( - setting: string, - value?: string | number - ): Promise => { - mixpanel.track("settings_changed", { - distinct_id: await getClientId(), - version: await getVersion(), - setting, - value, - }) - }, + init: async (): Promise => { + mixpanel.track("app_init", { + distinct_id: await getClientId(), + version: await getVersion(), + }) + }, + open_about: async (): Promise => { + mixpanel.track("open_about", { + distinct_id: await getClientId(), + version: await getVersion(), + }) + }, + open_settings: async (): Promise => { + mixpanel.track("open_settings", { + distinct_id: await getClientId(), + version: await getVersion(), + }) + }, + settings_changed: async ( + setting: string, + value?: string | number + ): Promise => { + mixpanel.track("settings_changed", { + distinct_id: await getClientId(), + version: await getVersion(), + setting, + value, + }) + }, } export default events diff --git a/src/mixpanel/propertyGetters.tsx b/src/mixpanel/propertyGetters.tsx index 7296026..28e4df7 100644 --- a/src/mixpanel/propertyGetters.tsx +++ b/src/mixpanel/propertyGetters.tsx @@ -4,17 +4,17 @@ import { getVersion as getVersionTauri } from "@tauri-apps/api/app" let clientId: string export const getClientId = async () => { - if (clientId === undefined) { - clientId = (await invoke("get_machine_id")) as string - } - return clientId + if (clientId === undefined) { + clientId = (await invoke("get_machine_id")) as string + } + return clientId } let version: string export const getVersion = async () => { - if (version === undefined) { - version = await getVersionTauri() - } - return version + if (version === undefined) { + version = await getVersionTauri() + } + return version } diff --git a/src/streamer-overlay/SPECIAL-REACT/HTML.tsx b/src/streamer-overlay/SPECIAL-REACT/HTML.tsx index 7abd30d..3828d24 100644 --- a/src/streamer-overlay/SPECIAL-REACT/HTML.tsx +++ b/src/streamer-overlay/SPECIAL-REACT/HTML.tsx @@ -1,7 +1,7 @@ import React from "react" export interface HTMLProps { - html: string + html: string } /** @@ -9,44 +9,41 @@ export interface HTMLProps { * This react component is meant for the streamerOverlay where only inline styles work! */ export const HTML: React.FC = ({ html }) => { - return ( - - - - - - - - - - Coh3 Stats Desktop App Overlay - - - -
- - - ) + + + +
+ + + ) } /* diff --git a/src/streamer-overlay/SPECIAL-REACT/OverlayApp.tsx b/src/streamer-overlay/SPECIAL-REACT/OverlayApp.tsx index 173cb74..e65f5fd 100644 --- a/src/streamer-overlay/SPECIAL-REACT/OverlayApp.tsx +++ b/src/streamer-overlay/SPECIAL-REACT/OverlayApp.tsx @@ -3,9 +3,9 @@ import { FullGameData } from "../../game-data-provider/GameData" import { PlayerEntry } from "./PlayerEntry" export interface OverlayAppProps { - gameData: FullGameData - flags: boolean - alwaysVisible: boolean + gameData: FullGameData + flags: boolean + alwaysVisible: boolean } // gameData.state === "Loading" || gameData.state === "InGame" @@ -15,61 +15,61 @@ export interface OverlayAppProps { * This react component is meant for the streamerOverlay where only inline styles work! */ export const OverlayApp: React.FC = ({ - gameData, - flags, - alwaysVisible, + gameData, + flags, + alwaysVisible, }) => { - return ( - <> - {alwaysVisible || - gameData.state === "Loading" || - gameData.state === "InGame" ? ( -
-
- {gameData.left.players.map((player, index) => ( - - ))} -
-
- {gameData.right.players.map((player, index) => ( - - ))} -
-
- ) : null} - - ) + return ( + <> + {alwaysVisible || + gameData.state === "Loading" || + gameData.state === "InGame" ? ( +
+
+ {gameData.left.players.map((player, index) => ( + + ))} +
+
+ {gameData.right.players.map((player, index) => ( + + ))} +
+
+ ) : null} + + ) } diff --git a/src/streamer-overlay/SPECIAL-REACT/PlayerEntry.tsx b/src/streamer-overlay/SPECIAL-REACT/PlayerEntry.tsx index 8541e13..0055fa3 100644 --- a/src/streamer-overlay/SPECIAL-REACT/PlayerEntry.tsx +++ b/src/streamer-overlay/SPECIAL-REACT/PlayerEntry.tsx @@ -2,8 +2,8 @@ import React from "react" import { FullPlayerData } from "../../game-data-provider/GameData" export interface PlayerEntryProps { - playerData: FullPlayerData - flags?: boolean + playerData: FullPlayerData + flags?: boolean } /** @@ -11,74 +11,74 @@ export interface PlayerEntryProps { * This react component is meant for the streamerOverlay where only inline styles work! */ export const PlayerEntry: React.FC = ({ - playerData, - flags = false, + playerData, + flags = false, }) => { - return ( -
- - {flags ? ( - - ) : null} - - {playerData.rank === undefined || playerData.rank === -1 - ? "-" - : "#" + playerData.rank} - {" "} - - {playerData.rating === undefined || playerData.rating === -1 - ? "-" - : playerData.rating} - {" "} - - {playerData.name} - -
- ) + return ( +
+ + {flags ? ( + + ) : null} + + {playerData.rank === undefined || playerData.rank === -1 + ? "-" + : "#" + playerData.rank} + {" "} + + {playerData.rating === undefined || playerData.rating === -1 + ? "-" + : playerData.rating} + {" "} + + {playerData.name} + +
+ ) } diff --git a/src/streamer-overlay/configValues.tsx b/src/streamer-overlay/configValues.tsx index a2095c9..9352e70 100644 --- a/src/streamer-overlay/configValues.tsx +++ b/src/streamer-overlay/configValues.tsx @@ -1,16 +1,16 @@ import { configValueFactory } from "../config-store/configValueFactory" const [getShowFlagsOverlay, useShowFlagsOverlay] = configValueFactory( - "showFlagsOverlay", - async () => false + "showFlagsOverlay", + async () => false ) const [getAlwaysShowOverlay, useAlwaysShowOverlay] = - configValueFactory("alwaysShowOverlay", async () => false) + configValueFactory("alwaysShowOverlay", async () => false) export { - getShowFlagsOverlay, - useShowFlagsOverlay, - getAlwaysShowOverlay, - useAlwaysShowOverlay, + getShowFlagsOverlay, + useShowFlagsOverlay, + getAlwaysShowOverlay, + useAlwaysShowOverlay, } diff --git a/src/streamer-overlay/renderStreamerOverlay.tsx b/src/streamer-overlay/renderStreamerOverlay.tsx index bae7196..7537310 100644 --- a/src/streamer-overlay/renderStreamerOverlay.tsx +++ b/src/streamer-overlay/renderStreamerOverlay.tsx @@ -6,15 +6,15 @@ import { HTML } from "./SPECIAL-REACT/HTML" import { getShowFlagsOverlay, getAlwaysShowOverlay } from "./configValues" export const renderStreamerHTML = async (gameData: FullGameData) => { - const content = renderToString( - - ) - const html = renderToStaticMarkup() - await writeTextFile("streamerOverlay.html", `\n${html}`, { - dir: BaseDirectory.AppData, - }) + const content = renderToString( + + ) + const html = renderToStaticMarkup() + await writeTextFile("streamerOverlay.html", `\n${html}`, { + dir: BaseDirectory.AppData, + }) }