From e3bd271abe93b9a9e70bd0e8d83547a124a8cb0d Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Thu, 26 Feb 2026 20:27:05 +0100 Subject: [PATCH] Add TheRun.gg Integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LiveSplit One now supports integration with [therun.gg](https://therun.gg) for live run tracking and automatic stats uploading. Changes: - **TheRun.gg settings**: A new "TheRun.gg Integration" section in the Network settings lets users enter their upload key (obtained from [therun.gg/livesplit](https://therun.gg/livesplit)) and toggle live tracking and stats uploading independently. - **Password field**: The upload key is masked by default with a visibility toggle button. The browser's password save prompt is suppressed via `autoComplete="off"`. - **TheRunClient integration**: A `TheRunClient` is created from the settings and receives timer events (`handleEvent`) for live tracking and stats uploading. - **Settings persistence**: The three therun.gg fields are stored as a single `TheRunGgSettings` object in IndexedDB. - **Localization**: All therun.gg labels translated in all supported languages. Changelog (en): LiveSplit One now supports [TheRun.gg](https://therun.gg) integration! Enter your upload key in the Network settings to enable live run tracking and automatic stats uploading. Changelog (de): LiveSplit One unterstützt jetzt die Integration mit [TheRun.gg](https://therun.gg)! Gib deinen Upload-Schlüssel in den Netzwerk-Einstellungen ein, um Live-Run-Tracking und automatisches Hochladen von Statistiken zu aktivieren. Changelog (fr): LiveSplit One prend désormais en charge l'intégration avec [TheRun.gg](https://therun.gg) ! Entrez votre clé d'envoi dans les paramètres réseau pour activer le suivi en direct et l'envoi automatique des statistiques. Changelog (nl): LiveSplit One ondersteunt nu integratie met [TheRun.gg](https://therun.gg)! Voer je uploadsleutel in bij de netwerkinstellingen om live run-tracking en automatisch uploaden van statistieken in te schakelen. Changelog (es): ¡LiveSplit One ahora soporta la integración con [TheRun.gg](https://therun.gg)! Introduce tu clave de subida en la configuración de red para activar el seguimiento en vivo y la subida automática de estadísticas. Changelog (it): LiveSplit One ora supporta l'integrazione con [TheRun.gg](https://therun.gg)! Inserisci la tua chiave di upload nelle impostazioni di rete per abilitare il tracciamento live e il caricamento automatico delle statistiche. Changelog (pt): O LiveSplit One agora suporta integração com o [TheRun.gg](https://therun.gg)! Introduza a sua chave de upload nas definições de rede para ativar o rastreamento ao vivo e o envio automático de estatísticas. Changelog (pt-BR): O LiveSplit One agora suporta integração com o [TheRun.gg](https://therun.gg)! Insira sua chave de upload nas configurações de rede para ativar o rastreamento ao vivo e o envio automático de estatísticas. Changelog (pl): LiveSplit One obsługuje teraz integrację z [TheRun.gg](https://therun.gg)! Wprowadź swój klucz przesyłania w ustawieniach sieci, aby włączyć śledzenie biegu na żywo i automatyczne przesyłanie statystyk. Changelog (ru): LiveSplit One теперь поддерживает интеграцию с [TheRun.gg](https://therun.gg)! Введите ключ загрузки в настройках сети, чтобы включить отслеживание забегов в реальном времени и автоматическую загрузку статистики. Changelog (ja): LiveSplit One が [TheRun.gg](https://therun.gg) との連携に対応しました!ネットワーク設定でアップロードキーを入力すると、ランのライブ追跡と統計の自動アップロードが有効になります。 Changelog (ko): LiveSplit One이 이제 [TheRun.gg](https://therun.gg) 통합을 지원합니다! 네트워크 설정에서 업로드 키를 입력하면 런 실시간 추적과 통계 자동 업로드를 활성화할 수 있습니다. Changelog (zh-Hans): LiveSplit One 现已支持 [TheRun.gg](https://therun.gg) 集成!在网络设置中输入上传密钥即可启用实时跑图追踪和统计数据自动上传。 Changelog (zh-Hant): LiveSplit One 現已支持 [TheRun.gg](https://therun.gg) 集成!在網路設定中輸入上傳密鑰即可啟用即時跑圖追蹤和統計資料自動上傳。 --- buildCore.js | 2 +- livesplit-core | 2 +- src-tauri/Cargo.lock | 39 ++++---- src/localization/chinese-simplified.ts | 6 ++ src/localization/dutch.ts | 6 ++ src/localization/english.ts | 6 ++ src/localization/french.ts | 6 ++ src/localization/german.ts | 6 ++ src/localization/index.ts | 6 ++ src/localization/italian.ts | 6 ++ src/localization/japanese.ts | 6 ++ src/localization/korean.ts | 6 ++ src/localization/polish.ts | 6 ++ src/localization/portuguese-brazil.ts | 6 ++ src/localization/portuguese.ts | 6 ++ src/localization/russian.ts | 6 ++ src/localization/spanish.ts | 6 ++ src/storage/index.ts | 1 + src/ui/LiveSplit.tsx | 32 ++++++ src/ui/components/Settings/String.tsx | 55 ++++++++++- src/ui/components/Settings/index.tsx | 13 ++- src/ui/views/MainSettings.tsx | 130 ++++++++++++++++++++++++- 22 files changed, 333 insertions(+), 25 deletions(-) diff --git a/buildCore.js b/buildCore.js index 43c0d787b..f1635fc2f 100644 --- a/buildCore.js +++ b/buildCore.js @@ -63,7 +63,7 @@ execSync(`cargo ${toolchain} run`, { }); execSync( - `cargo ${toolchain} rustc -p livesplit-core-capi --crate-type cdylib --features wasm-web,web-rendering --target ${target} ${cargoFlags}`, + `cargo ${toolchain} rustc -p livesplit-core-capi --crate-type cdylib --features wasm-web,web-rendering,therun-gg --target ${target} ${cargoFlags}`, { cwd: "livesplit-core", stdio: "inherit", diff --git a/livesplit-core b/livesplit-core index 1f6344491..d64114013 160000 --- a/livesplit-core +++ b/livesplit-core @@ -1 +1 @@ -Subproject commit 1f6344491cbe75b2eb78d0c04adf39cbf8f78ed0 +Subproject commit d64114013d140b1b6fb581d7d640136af162a43a diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e532c9f6a..2219c65da 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -284,18 +284,18 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -2126,9 +2126,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -2433,9 +2433,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -4695,31 +4695,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.43" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "libc", "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4727,9 +4728,9 @@ dependencies = [ [[package]] name = "tiny-skia" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" dependencies = [ "arrayref", "arrayvec", @@ -4741,9 +4742,9 @@ dependencies = [ [[package]] name = "tiny-skia-path" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" dependencies = [ "arrayref", "bytemuck", diff --git a/src/localization/chinese-simplified.ts b/src/localization/chinese-simplified.ts index 1e598e4e5..a5f91fff7 100644 --- a/src/localization/chinese-simplified.ts +++ b/src/localization/chinese-simplified.ts @@ -36,6 +36,12 @@ export function resolveChineseSimplified(text: Label): string { case Label.ServerConnection: return "服务器连接"; case Label.ServerConnectionDescription: return "允许你连接到 WebSocket 服务器,通过发送各种命令来控制计时器。目前这些命令是原版 LiveSplit 支持命令的子集。"; case Label.ServerConnectionExperimental: return "此功能为实验性,协议未来可能会更改。"; + case Label.TheRunGgIntegration: return "TheRun.gg 集成"; + case Label.TheRunGgIntegrationDescription: return "与 therun.gg 集成,用于实时追踪跑图并自动上传统计数据。使用此功能需要 therun.gg 的上传密钥。"; + case Label.TheRunGgLiveTracking: return "实时追踪"; + case Label.TheRunGgLiveTrackingDescription: return "启用后,每次分段操作后都会将实时分段数据发送到 therun.gg,从而可以在网站上实时追踪跑图。"; + case Label.TheRunGgStatsUploading: return "统计上传"; + case Label.TheRunGgStatsUploadingDescription: return "启用后,每次重置或完成跑图后都会将分段文件上传到 therun.gg,自动将你的跑图与网站同步。"; case Label.HotkeyAlreadyInUse: return "该热键已被使用。"; case Label.Start: return "开始"; case Label.Resume: return "继续"; diff --git a/src/localization/dutch.ts b/src/localization/dutch.ts index 5f239d406..e931ccae3 100644 --- a/src/localization/dutch.ts +++ b/src/localization/dutch.ts @@ -36,6 +36,12 @@ export function resolveDutch(text: Label): string { case Label.ServerConnection: return "Serververbinding"; case Label.ServerConnectionDescription: return "Staat toe om verbinding te maken met een WebSocket-server die de timer kan besturen door verschillende opdrachten te verzenden. De opdrachten zijn momenteel een subset van de opdrachten die de originele LiveSplit ondersteunt."; case Label.ServerConnectionExperimental: return "Deze functie is experimenteel en het protocol zal waarschijnlijk in de toekomst veranderen."; + case Label.TheRunGgIntegration: return "TheRun.gg-integratie"; + case Label.TheRunGgIntegrationDescription: return "Integreert met therun.gg voor live run-tracking en automatisch uploaden van statistieken. Je hebt een uploadsleutel van therun.gg nodig om deze functie te gebruiken."; + case Label.TheRunGgLiveTracking: return "Live tracking"; + case Label.TheRunGgLiveTrackingDescription: return "Wanneer ingeschakeld, worden live splitgegevens na elke splitactie naar therun.gg gestuurd, zodat de run live op de website gevolgd kan worden."; + case Label.TheRunGgStatsUploading: return "Statistieken uploaden"; + case Label.TheRunGgStatsUploadingDescription: return "Wanneer ingeschakeld, wordt het splitsbestand na elke reset of voltooide run naar therun.gg geüpload, zodat je runs automatisch met de site worden gesynchroniseerd."; case Label.HotkeyAlreadyInUse: return "De sneltoets is al in gebruik."; case Label.Start: return "Start"; case Label.Resume: return "Hervatten"; diff --git a/src/localization/english.ts b/src/localization/english.ts index 53e053fe7..6dcd0be47 100644 --- a/src/localization/english.ts +++ b/src/localization/english.ts @@ -36,6 +36,12 @@ export function resolveEnglish(text: Label): string { case Label.ServerConnection: return "Server Connection"; case Label.ServerConnectionDescription: return "Allows you to connect to a WebSocket server that can control the timer by sending various commands. The commands are currently a subset of the commands the original LiveSplit supports."; case Label.ServerConnectionExperimental: return "This feature is experimental and the protocol will likely change in the future."; + case Label.TheRunGgIntegration: return "TheRun.gg Integration"; + case Label.TheRunGgIntegrationDescription: return "Integrates with therun.gg for live run tracking and automatic stats uploading. You need an upload key from therun.gg to use this feature."; + case Label.TheRunGgLiveTracking: return "Live Tracking"; + case Label.TheRunGgLiveTrackingDescription: return "When enabled, live split data is sent to therun.gg after every split action, enabling live run tracking on the website."; + case Label.TheRunGgStatsUploading: return "Stats Uploading"; + case Label.TheRunGgStatsUploadingDescription: return "When enabled, the splits file is uploaded to therun.gg after every reset or finished run, automatically syncing your runs with the site."; case Label.HotkeyAlreadyInUse: return "The hotkey is already in use."; case Label.Start: return "Start"; case Label.Resume: return "Resume"; diff --git a/src/localization/french.ts b/src/localization/french.ts index e947df6c5..3fd1595bf 100644 --- a/src/localization/french.ts +++ b/src/localization/french.ts @@ -36,6 +36,12 @@ export function resolveFrench(text: Label): string { case Label.ServerConnection: return "Connexion au serveur"; case Label.ServerConnectionDescription: return "Permet de se connecter à un serveur WebSocket qui peut contrôler le minuteur en envoyant diverses commandes. Les commandes sont actuellement un sous‑ensemble de celles que le LiveSplit original prend en charge."; case Label.ServerConnectionExperimental: return "Cette fonctionnalité est expérimentale et le protocole changera probablement à l’avenir."; + case Label.TheRunGgIntegration: return "Intégration TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "S’intègre à therun.gg pour le suivi en direct des runs et l’envoi automatique des statistiques. Vous avez besoin d’une clé d’envoi therun.gg pour utiliser cette fonctionnalité."; + case Label.TheRunGgLiveTracking: return "Suivi en direct"; + case Label.TheRunGgLiveTrackingDescription: return "Lorsqu’il est activé, les données de split en direct sont envoyées à therun.gg après chaque action de split, ce qui permet un suivi en direct sur le site."; + case Label.TheRunGgStatsUploading: return "Envoi des statistiques"; + case Label.TheRunGgStatsUploadingDescription: return "Lorsqu’il est activé, le fichier de splits est envoyé à therun.gg après chaque réinitialisation ou run terminée, ce qui synchronise automatiquement vos runs avec le site."; case Label.HotkeyAlreadyInUse: return "Le raccourci est déjà utilisé."; case Label.Start: return "Démarrer"; case Label.Resume: return "Reprendre"; diff --git a/src/localization/german.ts b/src/localization/german.ts index 1739aaa8c..069bc53d1 100644 --- a/src/localization/german.ts +++ b/src/localization/german.ts @@ -36,6 +36,12 @@ export function resolveGerman(text: Label): string { case Label.ServerConnection: return "Serververbindung"; case Label.ServerConnectionDescription: return "Ermöglicht die Verbindung zu einem WebSocket‑Server, der den Timer durch das Senden verschiedener Befehle steuern kann. Die Befehle sind derzeit eine Teilmenge der Befehle, die das ursprüngliche LiveSplit unterstützt."; case Label.ServerConnectionExperimental: return "Diese Funktion ist experimentell und das Protokoll wird sich wahrscheinlich in Zukunft ändern."; + case Label.TheRunGgIntegration: return "TheRun.gg-Integration"; + case Label.TheRunGgIntegrationDescription: return "Integriert sich mit therun.gg für Live-Run-Tracking und automatisches Hochladen von Statistiken. Für diese Funktion benötigst du einen Upload-Schlüssel von therun.gg."; + case Label.TheRunGgLiveTracking: return "Live-Tracking"; + case Label.TheRunGgLiveTrackingDescription: return "Wenn aktiviert, werden Live-Split-Daten nach jeder Split-Aktion an therun.gg gesendet, sodass der Run live auf der Website verfolgt werden kann."; + case Label.TheRunGgStatsUploading: return "Statistik-Upload"; + case Label.TheRunGgStatsUploadingDescription: return "Wenn aktiviert, wird die Splits-Datei nach jedem Reset oder abgeschlossenen Run zu therun.gg hochgeladen und deine Runs werden automatisch mit der Website synchronisiert."; case Label.HotkeyAlreadyInUse: return "Der Hotkey wird bereits verwendet."; case Label.Start: return "Start"; case Label.Resume: return "Fortsetzen"; diff --git a/src/localization/index.ts b/src/localization/index.ts index 6d89caca6..1e804ea79 100644 --- a/src/localization/index.ts +++ b/src/localization/index.ts @@ -49,6 +49,12 @@ export enum Label { ServerConnection, ServerConnectionDescription, ServerConnectionExperimental, + TheRunGgIntegration, + TheRunGgIntegrationDescription, + TheRunGgLiveTracking, + TheRunGgLiveTrackingDescription, + TheRunGgStatsUploading, + TheRunGgStatsUploadingDescription, HotkeyAlreadyInUse, Start, Resume, diff --git a/src/localization/italian.ts b/src/localization/italian.ts index 053282f5e..099e1aeea 100644 --- a/src/localization/italian.ts +++ b/src/localization/italian.ts @@ -36,6 +36,12 @@ export function resolveItalian(text: Label): string { case Label.ServerConnection: return "Connessione server"; case Label.ServerConnectionDescription: return "Consente di connettersi a un server WebSocket che può controllare il timer inviando vari comandi. I comandi sono attualmente un sottoinsieme di quelli supportati dal LiveSplit originale."; case Label.ServerConnectionExperimental: return "Questa funzionalità è sperimentale e il protocollo probabilmente cambierà in futuro."; + case Label.TheRunGgIntegration: return "Integrazione TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "Si integra con therun.gg per il tracciamento live delle run e il caricamento automatico delle statistiche. Per usare questa funzionalità è necessaria una chiave di upload di therun.gg."; + case Label.TheRunGgLiveTracking: return "Tracciamento live"; + case Label.TheRunGgLiveTrackingDescription: return "Quando è attivo, i dati live degli split vengono inviati a therun.gg dopo ogni azione di split, consentendo il tracciamento in tempo reale sul sito."; + case Label.TheRunGgStatsUploading: return "Caricamento statistiche"; + case Label.TheRunGgStatsUploadingDescription: return "Quando è attivo, il file degli split viene caricato su therun.gg dopo ogni reset o run completata, sincronizzando automaticamente le tue run con il sito."; case Label.HotkeyAlreadyInUse: return "La scorciatoia è già in uso."; case Label.Start: return "Avvia"; case Label.Resume: return "Riprendi"; diff --git a/src/localization/japanese.ts b/src/localization/japanese.ts index 906f318c3..c2b68b98c 100644 --- a/src/localization/japanese.ts +++ b/src/localization/japanese.ts @@ -36,6 +36,12 @@ export function resolveJapanese(text: Label): string { case Label.ServerConnection: return "サーバー接続"; case Label.ServerConnectionDescription: return "WebSocket サーバーに接続して、さまざまなコマンドを送信することでタイマーを制御できます。これらのコマンドは現在、元の LiveSplit がサポートするコマンドの一部です。"; case Label.ServerConnectionExperimental: return "この機能は実験的で、プロトコルは将来変更される可能性があります。"; + case Label.TheRunGgIntegration: return "TheRun.gg 連携"; + case Label.TheRunGgIntegrationDescription: return "therun.gg と連携し、ランのライブ追跡と統計情報の自動アップロードを行います。この機能を使うには therun.gg のアップロードキーが必要です。"; + case Label.TheRunGgLiveTracking: return "ライブ追跡"; + case Label.TheRunGgLiveTrackingDescription: return "有効にすると、スプリット操作のたびにライブスプリットデータが therun.gg に送信され、サイト上でランをライブ追跡できます。"; + case Label.TheRunGgStatsUploading: return "統計のアップロード"; + case Label.TheRunGgStatsUploadingDescription: return "有効にすると、リセット時またはラン完了時にスプリットファイルが therun.gg にアップロードされ、ランがサイトと自動的に同期されます。"; case Label.HotkeyAlreadyInUse: return "このホットキーは既に使用されています。"; case Label.Start: return "開始"; case Label.Resume: return "再開"; diff --git a/src/localization/korean.ts b/src/localization/korean.ts index b1606b5a0..bf333fedd 100644 --- a/src/localization/korean.ts +++ b/src/localization/korean.ts @@ -36,6 +36,12 @@ export function resolveKorean(text: Label): string { case Label.ServerConnection: return "서버 연결"; case Label.ServerConnectionDescription: return "WebSocket 서버에 연결하여 다양한 명령을 전송함으로써 타이머를 제어할 수 있습니다. 현재 명령은 원래 LiveSplit이 지원하는 명령의 일부입니다."; case Label.ServerConnectionExperimental: return "이 기능은 실험적이며 프로토콜은 향후 변경될 가능성이 있습니다."; + case Label.TheRunGgIntegration: return "TheRun.gg 통합"; + case Label.TheRunGgIntegrationDescription: return "therun.gg와 통합하여 런 실시간 추적과 통계 자동 업로드를 제공합니다. 이 기능을 사용하려면 therun.gg 업로드 키가 필요합니다."; + case Label.TheRunGgLiveTracking: return "실시간 추적"; + case Label.TheRunGgLiveTrackingDescription: return "활성화하면 스플릿 동작을 할 때마다 실시간 스플릿 데이터가 therun.gg로 전송되어 웹사이트에서 런을 실시간으로 추적할 수 있습니다."; + case Label.TheRunGgStatsUploading: return "통계 업로드"; + case Label.TheRunGgStatsUploadingDescription: return "활성화하면 리셋하거나 런을 완료할 때마다 스플릿 파일이 therun.gg에 업로드되어 런 기록이 웹사이트와 자동으로 동기화됩니다."; case Label.HotkeyAlreadyInUse: return "해당 단축키는 이미 사용 중입니다."; case Label.Start: return "시작"; case Label.Resume: return "재개"; diff --git a/src/localization/polish.ts b/src/localization/polish.ts index 4a6d25b1a..b2e08c4ae 100644 --- a/src/localization/polish.ts +++ b/src/localization/polish.ts @@ -36,6 +36,12 @@ export function resolvePolish(text: Label): string { case Label.ServerConnection: return "Połączenie z serwerem"; case Label.ServerConnectionDescription: return "Pozwala połączyć się z serwerem WebSocket, który może sterować timerem poprzez wysyłanie różnych poleceń. Polecenia są obecnie podzbiorem poleceń obsługiwanych przez oryginalny LiveSplit."; case Label.ServerConnectionExperimental: return "Ta funkcja jest eksperymentalna i protokół prawdopodobnie zmieni się w przyszłości."; + case Label.TheRunGgIntegration: return "Integracja z TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "Integruje z therun.gg, zapewniając śledzenie biegu na żywo oraz automatyczne przesyłanie statystyk. Aby korzystać z tej funkcji, potrzebujesz klucza przesyłania z therun.gg."; + case Label.TheRunGgLiveTracking: return "Śledzenie na żywo"; + case Label.TheRunGgLiveTrackingDescription: return "Po włączeniu dane splitów na żywo są wysyłane do therun.gg po każdej akcji splitu, co umożliwia śledzenie biegu na żywo na stronie."; + case Label.TheRunGgStatsUploading: return "Przesyłanie statystyk"; + case Label.TheRunGgStatsUploadingDescription: return "Po włączeniu plik splitów jest przesyłany do therun.gg po każdym resecie lub ukończonym biegu, automatycznie synchronizując biegi ze stroną."; case Label.HotkeyAlreadyInUse: return "Ten skrót klawiszowy jest już używany."; case Label.Start: return "Start"; case Label.Resume: return "Wznów"; diff --git a/src/localization/portuguese-brazil.ts b/src/localization/portuguese-brazil.ts index d4ff1a0a0..78df60b7e 100644 --- a/src/localization/portuguese-brazil.ts +++ b/src/localization/portuguese-brazil.ts @@ -36,6 +36,12 @@ export function resolveBrazilianPortuguese(text: Label): string { case Label.ServerConnection: return "Conexão com servidor"; case Label.ServerConnectionDescription: return "Permite conectar a um servidor WebSocket que pode controlar o timer enviando vários comandos. Os comandos atualmente são um subconjunto dos comandos suportados pelo LiveSplit original."; case Label.ServerConnectionExperimental: return "Este recurso é experimental e o protocolo provavelmente mudará no futuro."; + case Label.TheRunGgIntegration: return "Integração com TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "Integra-se ao therun.gg para rastreamento ao vivo das runs e envio automático de estatísticas. Você precisa de uma chave de upload do therun.gg para usar este recurso."; + case Label.TheRunGgLiveTracking: return "Rastreamento ao vivo"; + case Label.TheRunGgLiveTrackingDescription: return "Quando ativado, os dados de split ao vivo são enviados para o therun.gg após cada ação de split, permitindo o rastreamento da run em tempo real no site."; + case Label.TheRunGgStatsUploading: return "Envio de estatísticas"; + case Label.TheRunGgStatsUploadingDescription: return "Quando ativado, o arquivo de splits é enviado ao therun.gg após cada reset ou run concluída, sincronizando automaticamente suas runs com o site."; case Label.HotkeyAlreadyInUse: return "A tecla de atalho já está em uso."; case Label.Start: return "Iniciar"; case Label.Resume: return "Retomar"; diff --git a/src/localization/portuguese.ts b/src/localization/portuguese.ts index 2791d45c5..b5d5ef52b 100644 --- a/src/localization/portuguese.ts +++ b/src/localization/portuguese.ts @@ -36,6 +36,12 @@ export function resolvePortuguese(text: Label): string { case Label.ServerConnection: return "Conexão com servidor"; case Label.ServerConnectionDescription: return "Permite conectar a um servidor WebSocket que pode controlar o timer enviando vários comandos. Os comandos atualmente são um subconjunto dos comandos suportados pelo LiveSplit original."; case Label.ServerConnectionExperimental: return "Este recurso é experimental e o protocolo provavelmente mudará no futuro."; + case Label.TheRunGgIntegration: return "Integração com TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "Integra-se ao therun.gg para rastreamento ao vivo das runs e envio automático de estatísticas. Você precisa de uma chave de upload do therun.gg para usar este recurso."; + case Label.TheRunGgLiveTracking: return "Rastreamento ao vivo"; + case Label.TheRunGgLiveTrackingDescription: return "Quando ativado, os dados de split ao vivo são enviados para o therun.gg após cada ação de split, permitindo o rastreamento da run em tempo real no site."; + case Label.TheRunGgStatsUploading: return "Envio de estatísticas"; + case Label.TheRunGgStatsUploadingDescription: return "Quando ativado, o arquivo de splits é enviado ao therun.gg após cada reset ou run concluída, sincronizando automaticamente suas runs com o site."; case Label.HotkeyAlreadyInUse: return "A tecla de atalho já está em uso."; case Label.Start: return "Iniciar"; case Label.Resume: return "Retomar"; diff --git a/src/localization/russian.ts b/src/localization/russian.ts index 2bfbebddd..a9db57bbb 100644 --- a/src/localization/russian.ts +++ b/src/localization/russian.ts @@ -36,6 +36,12 @@ export function resolveRussian(text: Label): string { case Label.ServerConnection: return "Подключение к серверу"; case Label.ServerConnectionDescription: return "Позволяет подключиться к серверу WebSocket, который может управлять таймером, отправляя различные команды. Команды сейчас являются подмножеством команд, поддерживаемых оригинальным LiveSplit."; case Label.ServerConnectionExperimental: return "Эта функция экспериментальная, и протокол, вероятно, изменится в будущем."; + case Label.TheRunGgIntegration: return "Интеграция с TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "Интегрируется с therun.gg для отслеживания забегов в реальном времени и автоматической загрузки статистики. Для использования этой функции нужен ключ загрузки из therun.gg."; + case Label.TheRunGgLiveTracking: return "Отслеживание в реальном времени"; + case Label.TheRunGgLiveTrackingDescription: return "Если включено, данные сплитов в реальном времени отправляются на therun.gg после каждого действия со сплитом, что позволяет отслеживать забег на сайте в реальном времени."; + case Label.TheRunGgStatsUploading: return "Загрузка статистики"; + case Label.TheRunGgStatsUploadingDescription: return "Если включено, файл сплитов загружается на therun.gg после каждого сброса или завершённого забега, автоматически синхронизируя ваши забеги с сайтом."; case Label.HotkeyAlreadyInUse: return "Эта горячая клавиша уже используется."; case Label.Start: return "Старт"; case Label.Resume: return "Продолжить"; diff --git a/src/localization/spanish.ts b/src/localization/spanish.ts index 426e1c1b3..33a588cd8 100644 --- a/src/localization/spanish.ts +++ b/src/localization/spanish.ts @@ -36,6 +36,12 @@ export function resolveSpanish(text: Label): string { case Label.ServerConnection: return "Conexión al servidor"; case Label.ServerConnectionDescription: return "Permite conectarte a un servidor WebSocket que puede controlar el temporizador enviando varios comandos. Los comandos actualmente son un subconjunto de los comandos compatibles con el LiveSplit original."; case Label.ServerConnectionExperimental: return "Esta función es experimental y es probable que el protocolo cambie en el futuro."; + case Label.TheRunGgIntegration: return "Integración con TheRun.gg"; + case Label.TheRunGgIntegrationDescription: return "Se integra con therun.gg para el seguimiento en vivo de tus runs y la subida automática de estadísticas. Necesitas una clave de subida de therun.gg para usar esta función."; + case Label.TheRunGgLiveTracking: return "Seguimiento en vivo"; + case Label.TheRunGgLiveTrackingDescription: return "Cuando está activado, los datos de splits en vivo se envían a therun.gg después de cada acción de split, lo que permite seguir la run en vivo en el sitio web."; + case Label.TheRunGgStatsUploading: return "Subida de estadísticas"; + case Label.TheRunGgStatsUploadingDescription: return "Cuando está activado, el archivo de splits se sube a therun.gg tras cada reinicio o run finalizada, sincronizando automáticamente tus runs con el sitio."; case Label.HotkeyAlreadyInUse: return "El atajo de teclado ya está en uso."; case Label.Start: return "Iniciar"; case Label.Resume: return "Reanudar"; diff --git a/src/storage/index.ts b/src/storage/index.ts index 8895b856d..670ec95ea 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -280,6 +280,7 @@ export async function loadGeneralSettings(): Promise { saveOnReset: generalSettings.saveOnReset ?? false, speedrunComIntegration: generalSettings.speedrunComIntegration ?? true, serverUrl: generalSettings.serverUrl, + theRunGgIntegration: generalSettings.theRunGgIntegration, alwaysOnTop: generalSettings.alwaysOnTop ?? (isTauri ? true : undefined), lang, diff --git a/src/ui/LiveSplit.tsx b/src/ui/LiveSplit.tsx index fd135ee55..eb87b8e42 100644 --- a/src/ui/LiveSplit.tsx +++ b/src/ui/LiveSplit.tsx @@ -49,6 +49,7 @@ import { UrlCache } from "../util/UrlCache"; import { HotkeySystem_add_window, ServerProtocol, + TheRunClient, WebRenderer, } from "../livesplit-core/livesplit_core"; import { LiveSplitServer } from "../api/LiveSplitServer"; @@ -68,6 +69,20 @@ import sidebarClasses from "../css/Sidebar.module.css"; import toastClasses from "../css/Toast.module.css"; import { Label, orAutoLang, resolve, setHtmlLang } from "../localization"; +function createTheRunClient( + generalSettings: GeneralSettings, +): Option { + const theRunGg = generalSettings.theRunGgIntegration; + if (theRunGg != null && theRunGg.uploadKey.length > 0) { + return new TheRunClient( + theRunGg.uploadKey, + theRunGg.liveTracking, + theRunGg.statsUploading, + ); + } + return null; +} + function getRootCssVar(name: string) { return getComputedStyle(document.documentElement) .getPropertyValue(name) @@ -141,6 +156,7 @@ export interface State { renderer: WebRenderer; generalSettings: GeneralSettings; serverConnection: Option; + theRunClient: Option; currentComparison: string; currentTimingMethod: TimingMethod; currentPhase: TimerPhase; @@ -262,6 +278,7 @@ export class LiveSplit extends React.Component { renderer, generalSettings: props.generalSettings, serverConnection: null, + theRunClient: createTheRunClient(props.generalSettings), currentComparison: commandSink.currentComparison(), currentTimingMethod: commandSink.currentTimingMethod(), currentPhase: commandSink.currentPhase(), @@ -378,6 +395,7 @@ export class LiveSplit extends React.Component { this.state.commandSink[Symbol.dispose](); this.state.layout[Symbol.dispose](); this.state.layoutState[Symbol.dispose](); + this.state.theRunClient?.[Symbol.dispose](); this.isDesktopQuery.removeEventListener( "change", @@ -840,6 +858,7 @@ export class LiveSplit extends React.Component { this.setState({ generalSettings }); this.updateTauriSettings(generalSettings); this.applyTheme(generalSettings); + this.updateTheRunClient(generalSettings); } else { setHtmlLang(this.state.generalSettings.lang); this.applyTheme(this.state.generalSettings); @@ -855,6 +874,15 @@ export class LiveSplit extends React.Component { }); } + private updateTheRunClient(generalSettings: GeneralSettings) { + const oldClient = this.state.theRunClient; + const newClient = createTheRunClient(generalSettings); + if (oldClient != null) { + oldClient[Symbol.dispose](); + } + this.setState({ theRunClient: newClient }); + } + public onResize(width: number, height: number) { this.setState( { @@ -1049,6 +1077,10 @@ export class LiveSplit extends React.Component { } handleEvent(event: Event): void { + this.state.theRunClient?.handleEvent( + event, + this.state.commandSink.getTimer(), + ); switch (event) { case Event.Started: this.splitsModifiedChanged(); diff --git a/src/ui/components/Settings/String.tsx b/src/ui/components/Settings/String.tsx index 1da2cabb7..0be66570e 100644 --- a/src/ui/components/Settings/String.tsx +++ b/src/ui/components/Settings/String.tsx @@ -1,6 +1,7 @@ import * as React from "react"; +import { useState } from "react"; import { SettingValueFactory } from "."; -import { Trash } from "lucide-react"; +import { Eye, EyeOff, Trash } from "lucide-react"; import { Switch } from "../Switch"; import { Label, resolve } from "../../../localization"; import { Language } from "../../../livesplit-core"; @@ -195,3 +196,55 @@ export function CustomVariable({ ); } } + +export function Password({ + value, + setValue, + factory, +}: { + value: string; + setValue: (value: T) => void; + factory: SettingValueFactory; +}) { + const [showPassword, setShowPassword] = useState(false); + + return ( +
+
+ + setValue(factory.fromString(e.target.value)) + } + /> + +
+
+ ); +} diff --git a/src/ui/components/Settings/index.tsx b/src/ui/components/Settings/index.tsx index e3ff46b6a..fb20e6597 100644 --- a/src/ui/components/Settings/index.tsx +++ b/src/ui/components/Settings/index.tsx @@ -29,6 +29,7 @@ import { Comparison, CustomVariable, OptionalString, + Password, RemovableString, String, } from "./String"; @@ -55,7 +56,7 @@ export interface ExtendedSettingsDescriptionJson { export interface ExtendedSettingsDescriptionFieldJson { text: string | React.JSX.Element; tooltip: string | React.JSX.Element; - hint?: "Comparison" | "CustomVariable"; + hint?: "Comparison" | "CustomVariable" | "Password"; value: ExtendedSettingsDescriptionValueJson; } @@ -328,6 +329,16 @@ export class SettingsComponent extends React.Component> { lang={this.props.lang} /> ); + } else if (field.hint === "Password") { + component = ( + + this.props.setValue(valueIndex, value) + } + factory={this.props.factory} + /> + ); } else { component = (

{resolve(Label.NetworkHeading, lang)}

+ {resolve(Label.TheRunGgIntegration, lang)} + + + + + ), + tooltip: resolve( + Label.TheRunGgIntegrationDescription, + lang, + ), + hint: "Password" as const, + value: { + String: + generalSettings.theRunGgIntegration + ?.uploadKey ?? "", + }, + }, + ...(generalSettings.theRunGgIntegration + ? [ + { + text: resolve( + Label.TheRunGgLiveTracking, + lang, + ), + tooltip: resolve( + Label.TheRunGgLiveTrackingDescription, + lang, + ), + value: { + Bool: generalSettings + .theRunGgIntegration.liveTracking, + }, + }, + { + text: resolve( + Label.TheRunGgStatsUploading, + lang, + ), + tooltip: resolve( + Label.TheRunGgStatsUploadingDescription, + lang, + ), + value: { + Bool: generalSettings + .theRunGgIntegration + .statsUploading, + }, + }, + ] + : []), ], }} editorUrlCache={urlCache} @@ -517,6 +591,58 @@ export function View({ }); } break; + case 2: + if ("String" in value) { + const uploadKey = value.String; + setGeneralSettings({ + ...generalSettings, + theRunGgIntegration: + uploadKey.length > 0 + ? { + uploadKey, + liveTracking: + generalSettings + .theRunGgIntegration + ?.liveTracking ?? + true, + statsUploading: + generalSettings + .theRunGgIntegration + ?.statsUploading ?? + true, + } + : undefined, + }); + } + break; + case 3: + if ( + "Bool" in value && + generalSettings.theRunGgIntegration + ) { + setGeneralSettings({ + ...generalSettings, + theRunGgIntegration: { + ...generalSettings.theRunGgIntegration, + liveTracking: value.Bool, + }, + }); + } + break; + case 4: + if ( + "Bool" in value && + generalSettings.theRunGgIntegration + ) { + setGeneralSettings({ + ...generalSettings, + theRunGgIntegration: { + ...generalSettings.theRunGgIntegration, + statsUploading: value.Bool, + }, + }); + } + break; } }} />