diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 9205394..74572f7 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -8,14 +8,14 @@ jobs: publish-tauri: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules-v1 with: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 63d9419..b755905 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -11,13 +11,13 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules-v1 with: @@ -44,14 +44,14 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - name: Cache node modules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules-v1 with: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d69a35f..569c995 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -99,6 +99,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -592,6 +598,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "ciborium" version = "0.2.1" @@ -730,6 +742,7 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-window-state", "thiserror", + "tiny_http", "tokio", "vault", "window-shadows", @@ -4847,6 +4860,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny_http" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca" +dependencies = [ + "ascii", + "chunked_transfer", + "log", + "time", + "url", +] + [[package]] name = "tinytemplate" version = "1.2.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2c91714..b2ca8ec 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,7 +25,7 @@ reqwest = { version = "0.11", features = ["json", "multipart"] } rev_lines = "0.3" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = [ "window-all", "path-all", "dialog-all", "shell-open", "clipboard-write-text", "http-all", "fs-all", "updater"] } +tauri = { version = "1.2", features = [ "process-all", "window-all", "path-all", "dialog-all", "shell-open", "clipboard-write-text", "http-all", "fs-all", "updater"] } tauri-plugin-deep-link = "0.1" tauri-plugin-fs-watch = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } @@ -38,6 +38,8 @@ vault = "8" window-shadows = "0.2" # monitoring by sentry sentry = "0.32.2" +# For the OBS interface we need to expose webserver +tiny_http = "0.11" [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } diff --git a/src-tauri/src/dp_utils.rs b/src-tauri/src/dp_utils.rs new file mode 100644 index 0000000..52405ae --- /dev/null +++ b/src-tauri/src/dp_utils.rs @@ -0,0 +1,31 @@ +use serde::de::DeserializeOwned; +// COH3 Desktop App Utils +use tauri_plugin_store::{StoreCollection, with_store}; +use log::{error}; +use tauri::{AppHandle, Manager, Runtime}; + +pub fn load_from_store(handle: AppHandle, key: &str) -> Option { + let stores = handle.state::>(); + let path = handle + .path_resolver() + .app_data_dir() + .unwrap() + .join("config.dat"); + + match with_store(handle.clone(), stores, path, |store| { + Ok(store.get(key).cloned()) + }) { + Ok(Some(value)) => match serde_json::from_value(value.clone()) { + Ok(result) => Some(result), + Err(err) => { + error!("error deserializing store value at {key}: {err}"); + None + } + }, + Ok(None) => None, + Err(err) => { + error!("error retrieving store value at {key}: {err}"); + None + } + } +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5cbdbc4..0f7a2c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,2 +1,4 @@ pub mod parse_log_file; pub mod plugins; +pub mod overlay_server; +pub mod dp_utils; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 413ca5e..f86e463 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,9 +5,11 @@ extern crate machine_uid; -use coh3_stats_desktop_app::{parse_log_file, plugins::cohdb}; -use log::error; +use coh3_stats_desktop_app::dp_utils::load_from_store; +use coh3_stats_desktop_app::{parse_log_file, plugins::cohdb, overlay_server::run_http_server}; +use log::{error, info}; use std::path::Path; +use std::thread; use tauri::Manager; use tauri_plugin_log::LogTarget; use window_shadows::set_shadow; @@ -62,8 +64,10 @@ fn main() { )) .plugin(coh3_stats_desktop_app::plugins::cohdb::sync::init()) .setup(setup) + .setup(setup_web_server) .run(tauri::generate_context!()) .expect("error while running tauri application"); + } fn setup(app: &mut tauri::App) -> Result<(), Box> { @@ -90,11 +94,30 @@ fn setup(app: &mut tauri::App) -> Result<(), Box> { Ok(()) } +fn setup_web_server(app: &mut tauri::App) -> Result<(), Box> { + let app_handle = app.handle(); + + if load_from_store(app_handle.clone(), "streamerOverlayEnabled").unwrap_or(false) { + info!("Streamer overlay is enabled"); + let mut file_path = app_handle.path_resolver().app_data_dir().unwrap(); + file_path.push("streamerOverlay.html"); + info!("Expecting the streamerOverlay at {:?}", file_path); + + let _handle = thread::spawn(|| { + run_http_server(file_path); + }); + } else { + info!("Streamer overlay is disabled"); + } + + Ok(()) +} + /// returns the default expected log file path #[tauri::command] fn default_log_file_path() -> String { let mut path = tauri::api::path::document_dir().unwrap(); - path.push("My Games"); + path.push("My Games"); // TODO: Is this "my games" also on non-English Windows? path.push("Company of Heroes 3"); path.push("warnings.log"); path.display().to_string() @@ -103,7 +126,7 @@ fn default_log_file_path() -> String { #[tauri::command] fn default_playback_path() -> String { let mut path = tauri::api::path::document_dir().unwrap(); - path.push("My Games"); + path.push("My Games"); // TODO: Is this "my games" also on non-English Windows? path.push("Company of Heroes 3"); path.push("playback"); path.display().to_string() diff --git a/src-tauri/src/overlay_server.rs b/src-tauri/src/overlay_server.rs new file mode 100644 index 0000000..ebbcb69 --- /dev/null +++ b/src-tauri/src/overlay_server.rs @@ -0,0 +1,37 @@ +use std::fs::File; +use std::panic; +use log::{error, info}; +use std::path::PathBuf; + +use tiny_http::{Server, Response, StatusCode}; + +pub fn run_http_server(streamer_overlay_path: PathBuf) { + + let result = panic::catch_unwind(|| { + + // Ideally we would allow setting up port in the settings + info!("Starting streamer overlay server on port 47824"); + let server = Server::http("127.0.0.1:47824").unwrap(); + + for request in server.incoming_requests() { + + let file = match File::open(&streamer_overlay_path) { + Ok(file) => file, + Err(_) => { + let response = Response::new_empty(StatusCode(404)); + let _ = request.respond(response); + continue; + } + }; + + let response = Response::from_file(file); + let _ = request.respond(response); + } + + }); + + if let Err(err) = result { + error!("Couldn't start the streamer overlay server: {:?}", err); + } + +} diff --git a/src-tauri/src/plugins/cohdb/sync/mod.rs b/src-tauri/src/plugins/cohdb/sync/mod.rs index 85c3c7e..2e5b2cd 100644 --- a/src-tauri/src/plugins/cohdb/sync/mod.rs +++ b/src-tauri/src/plugins/cohdb/sync/mod.rs @@ -3,14 +3,14 @@ use std::{path::PathBuf, sync::Mutex}; use auth::responses::User; use log::{debug, error, info, warn}; use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; -use serde::de::DeserializeOwned; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, EventHandler, Manager, Runtime, }; -use tauri_plugin_store::{with_store, StoreCollection}; use vault::{GameType, Replay}; +use crate::dp_utils::load_from_store; + use super::auth; #[derive(Debug, Default)] @@ -135,32 +135,6 @@ fn includes_user(replay: &Replay, user: &User) -> bool { .any(|player| player.profile_id().is_some() && player.profile_id() == user.profile_id) } -fn load_from_store(handle: AppHandle, key: &str) -> Option { - let stores = handle.state::>(); - let path = handle - .path_resolver() - .app_data_dir() - .unwrap() - .join("config.dat"); - - match with_store(handle.clone(), stores, path, |store| { - Ok(store.get(key).cloned()) - }) { - Ok(Some(value)) => match serde_json::from_value(value.clone()) { - Ok(result) => Some(result), - Err(err) => { - error!("error deserializing store value at {key}: {err}"); - None - } - }, - Ok(None) => None, - Err(err) => { - error!("error retrieving store value at {key}: {err}"); - None - } - } -} - fn load_playback_path(handle: AppHandle) -> String { if let Some(path) = load_from_store::(handle, "playbackPath") { path diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9d7b290..8545d38 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -33,6 +33,9 @@ }, "window": { "all": true + }, + "process": { + "all": true } }, "bundle": { diff --git a/src/main.tsx b/src/main.tsx index 5363e18..b9bd2b3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -14,10 +14,17 @@ info("Start frontend") Sentry.init({ dsn: "https://88e8a309f91b8b5bb9a41dd14ff775b9@o4504995920543744.ingest.sentry.io/4506752563019776", integrations: [Sentry.browserTracingIntegration()], - // Performance Monitoring - tracesSampleRate: 0.1, // Capture 100% of the transactions - // Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled + tracesSampleRate: 0.1, tracePropagationTargets: ["localhost"], + beforeSend(event, hint) { + // On macOS we do only development, we can ignore all development errors + if (event.contexts?.os?.name === "macOS") { + // Ignore the event + return null + } + // Otherwise, return the event as is + return event + }, }) events.init() diff --git a/src/streamer-overlay/configValues.tsx b/src/streamer-overlay/configValues.tsx index 9352e70..3a3a3ea 100644 --- a/src/streamer-overlay/configValues.tsx +++ b/src/streamer-overlay/configValues.tsx @@ -8,9 +8,14 @@ const [getShowFlagsOverlay, useShowFlagsOverlay] = configValueFactory( const [getAlwaysShowOverlay, useAlwaysShowOverlay] = configValueFactory("alwaysShowOverlay", async () => false) +const [getStreamerOverlayEnabled, useStreamerOverlayEnabled] = + configValueFactory("streamerOverlayEnabled", async () => false) + export { getShowFlagsOverlay, useShowFlagsOverlay, getAlwaysShowOverlay, useAlwaysShowOverlay, + getStreamerOverlayEnabled, + useStreamerOverlayEnabled, } diff --git a/src/views/About.tsx b/src/views/About.tsx index 973b969..aaf2844 100644 --- a/src/views/About.tsx +++ b/src/views/About.tsx @@ -1,4 +1,5 @@ import { getVersion } from "@tauri-apps/api/app" +import { appDataDir } from "@tauri-apps/api/path" import { open } from "@tauri-apps/api/shell" import { useState, useEffect } from "react" import { @@ -21,9 +22,12 @@ import events from "../mixpanel/mixpanel" export const About: React.FC = () => { const [appVersion, setAppVersion] = useState() + const [pathToLogs, setPathToLogs] = useState() + useEffect(() => { getVersion().then((version) => setAppVersion(version)) events.open_about() + appDataDir().then((path) => setPathToLogs(path)) }, []) return ( @@ -80,6 +84,12 @@ export const About: React.FC = () => { C:\Users\Username\Documents\My Games\Company of Heroes 3\warnings.log +
+
+ You can also provide the logs from the COH3 Stats Desktop app which + are located here: +
+ {pathToLogs}logs + + ) : null} + + +
{
-
Show flags:
{
- Path to streamerOverlay.html: - + Streamer overlay avaliable at: + { - writeText(appDataPath) + writeText("http://localhost:47824") }} > + +
+ + Local file has issue with refresh flicker, we recommend local + web server to avoid this issue. + + + Path to streamerOverlay.html: + + + { + writeText(appDataPath) + }} + > + + + + +
+