diff --git a/.claude/launch.json b/.claude/launch.json index 55f03cf..a1008f4 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -2,7 +2,7 @@ "version": "0.0.1", "configurations": [ { - "name": "tarkov-checker-dev", + "name": "raidmate-dev", "runtimeExecutable": "pnpm", "runtimeArgs": ["dev"], "port": 5173, diff --git a/.github/workflows/build-overlay.yml b/.github/workflows/build-overlay.yml index f9fc46f..4320381 100644 --- a/.github/workflows/build-overlay.yml +++ b/.github/workflows/build-overlay.yml @@ -45,8 +45,8 @@ jobs: - uses: actions/upload-artifact@v7 with: - name: tarkov-checker-desktop - path: apps/desktop/src-tauri/target/release/tarkov-checker-desktop.exe + name: raidmate + path: apps/desktop/src-tauri/target/release/raidmate.exe retention-days: 30 if-no-files-found: error @@ -54,7 +54,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v3 with: - files: apps/desktop/src-tauri/target/release/tarkov-checker-desktop.exe + files: apps/desktop/src-tauri/target/release/raidmate.exe tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} generate_release_notes: true diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index e96a078..efd82c0 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -10,7 +10,7 @@ name: Deploy frontend to GitHub Pages # the deploy step refuses to upload. # # The published URL is `https://.github.io//` for project -# pages — by default `https://mosmain.github.io/tarkov-checker/`. +# pages — by default `https://mosmain.github.io/raidmate/`. # Update `GITHUB_PAGES_BASE` in `apps/client/vite.config.ts` and # `ALLOWED_ORIGINS` in `apps/desktop/src-tauri/src/http_server.rs` if # the URL changes (rename, custom domain). @@ -47,15 +47,15 @@ jobs: - uses: ./.github/actions/install-js - # GITHUB_PAGES=true flips Vite's `base` to `/tarkov-checker/` and + # GITHUB_PAGES=true flips Vite's `base` to `/raidmate/` and # turns on the 404.html fallback plugin (SPA-history workaround # for Pages — see vite.config.ts). - name: Build SPA env: GITHUB_PAGES: 'true' run: | - pnpm --filter @tarkov-checker/shared build - pnpm --filter @tarkov-checker/client build + pnpm --filter @raidmate/shared build + pnpm --filter @raidmate/client build # Pages artifact is just a tarball of the dist/ folder; the deploy # job below unpacks it into the published site. diff --git a/CLAUDE.md b/CLAUDE.md index 0197da7..d968e57 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,10 +36,10 @@ One command: ``` -pnpm --filter @tarkov-checker/desktop tauri:dev +pnpm --filter @raidmate/desktop tauri:dev ``` -`beforeDevCommand` spawns `pnpm --filter @tarkov-checker/client dev` +`beforeDevCommand` spawns `pnpm --filter @raidmate/client dev` (Vite on **:5173**). Tauri waits for it, opens the webview at the same URL, and shuts Vite down when it exits. The Rust helper (in-process HTTP server) binds `0.0.0.0:47474` for `/api/*` and `/events`; Vite's @@ -51,7 +51,9 @@ Vite. Phones in the same Wi-Fi use `http://:5173/`. The embedded-SPA path lights up only in **release** builds, where there is no Vite. -The repo must live on an **ASCII path** (currently `C:\git-repos\tarkov-checker`) +The repo must live on an **ASCII path** (currently `C:\git-repos\raidmate`; +if the folder still carries the pre-rebrand name `tarkov-checker`, +renaming it is a manual step done outside any running session/editor) — `cargo metadata` segfaults on a Cyrillic CWD, which is what made the OneDrive Documents path (`C:\Users\\OneDrive\Документы\...`) unusable historically. Earlier the workaround was an NTFS junction @@ -293,7 +295,7 @@ map uses — no separate transport, no separate dataset. `apps/desktop/src-tauri/src/server/paths.rs` reads the registry via `winreg`, looks at env vars, applies manual overrides from -`%APPDATA%/tarkov-checker/config.json`, and returns the resolved triple +`%APPDATA%/raidmate/config.json`, and returns the resolved triple (`gameDir`, `logsDir`, `screenshotsDir`) with `source` + `exists` flags. Priority: env (`TARKOV_GAME_DIR`, `TARKOV_SCREENSHOT_DIR`) > manual @@ -361,7 +363,7 @@ Persisted state lives in **per-feature** Pinia stores, not one big store: `features/map/composables/useAutoMapSwitch.ts`, mounted once at `App.vue` root. - **Per-layer visibility**: `composables/useLayerVisibility.ts` exports a - function that returns a shared `persistedRef('tc.layer..visible', z.boolean(), true)` + function that returns a shared `persistedRef('rm.layer..visible', z.boolean(), true)` per layer id (module-level singleton). The LayerRail toggle and the layer composable read/write the same ref. Consumed by `MapLayerContext.visible` in `layers/registry.ts`. @@ -385,7 +387,7 @@ Persisted state lives in **per-feature** Pinia stores, not one big store: `phase` and `outcome` are derived runtime state, not persisted. Each store uses `persistedRef` from `@/shared/persisted-store` with its -own key (`tc..`) — corrupt persisted data falls back to +own key (`rm..`) — corrupt persisted data falls back to defaults silently. **Session-only state.** `clickThrough` intentionally does NOT use `persistedRef`. @@ -424,7 +426,7 @@ id, order, titleKey, visible?, component })`; `useSettingsSections()` returns th 40 paths (desktop-or-tauri), 50 pairing (tauri). Consumed by the gear **drawer** (`SettingsPanel.vue`): a **non-modal** PrimeVue `Drawer` (right on desktop, bottom-sheet on `<640px`) rendering a single flat **Accordion**; open-panel state - persists in `tc.settings.open`. + persists in `rm.settings.open`. Faction colours come from `FACTION_COLORS` in `packages/shared/src/maps.ts` so icons and tooltip stripes never drift across components. @@ -470,7 +472,7 @@ and the `apple-mobile-web-app-*` / `mobile-web-app-capable` meta in `index.html` `display: standalone`, icon = the existing `favicon.svg` (`sizes: "any"`). `start_url`/`scope` are `/`, correct for the phone (`:5173`/`:47474`) and the Tauri release (base `/`). The hosted **GitHub Pages** build serves under base -`/tarkov-checker/`, so its `start_url`/`scope` would mismatch — the static manifest +`/raidmate/`, so its `start_url`/`scope` would mismatch — the static manifest isn't templated by Vite. Pages PWA install is a secondary path; template the manifest if it ever matters. iOS `apple-touch-icon` ideally wants a PNG — pointing it at the SVG works on Android, but iPhone may fall back to a page screenshot for @@ -481,7 +483,7 @@ and the `apple-mobile-web-app-*` / `mobile-web-app-capable` meta in `index.html` `display: standalone`, icon = the existing `favicon.svg` (`sizes: "any"`). `start_url`/`scope` are `/`, correct for the phone (`:5173`/`:47474`) and the Tauri release (base `/`). The hosted **GitHub Pages** build serves under base -`/tarkov-checker/`, so its `start_url`/`scope` would mismatch — the static manifest +`/raidmate/`, so its `start_url`/`scope` would mismatch — the static manifest isn't templated by Vite. Pages PWA install is a secondary path; template the manifest if it ever matters. iOS `apple-touch-icon` ideally wants a PNG — pointing it at the SVG works on Android, but iPhone may fall back to a page screenshot for @@ -612,7 +614,7 @@ machine (via HTTP). One source of state, two transports. | `server/screenshots.rs` | `notify-debouncer-full` watcher (250 ms `awaitWriteFinish` equivalent); parses filename → position; opt-in recycle-bin delete after parse (see below) | | `server/logs.rs` | poll-tails latest `log_*/application_NNN.log`; emits `map-change` on `rcid:` / `Location:` hits | | `server/paths.rs` | env → manual override → `winreg` auto-detect; returns `ResolvedPaths` | -| `server/config.rs` | reads/writes `%APPDATA%/tarkov-checker/config.json`; rejects UNC paths | +| `server/config.rs` | reads/writes `%APPDATA%/raidmate/config.json`; rejects UNC paths | | `server/events.rs` | `ServerEvent` enum + `tokio::sync::broadcast` channel for HTTP-side fan-out | | `watcher.rs` | `WatcherSlot` state holder + `apply_resolved` that atomically swaps watcher handles | | `lan.rs` | LAN IP detection for the QR pairing flow (multi-NIC heuristic — see `detect_lan_ip`) | @@ -686,11 +688,11 @@ focus-required `useBrowserShortcut` and per-client `useGlobalShortcut` wiring for these is gone; only the overlay **lock** combo stays client-registered (see "Desktop overlay"). -- Combos persist in `%APPDATA%/tarkov-checker/hotkeys.json` via +- Combos persist in `%APPDATA%/raidmate/hotkeys.json` via `server/hotkeys.rs` (`HotkeyStore`, sibling to `config.json`). Defaults - match the client's historical `tc.hotkeys.*` so an un-customised install - is unchanged; the client migrates customised localStorage values up once - (`tc.hotkeys.migrated`). + match the client's `DEFAULTS` so an un-customised install agrees on both + ends. (The one-time localStorage→backend migration from the pre-rebrand + `tc.hotkeys.*` keys was removed in the RaidMate rebrand — clean break.) - Registration lives in `src/hotkeys.rs` behind one `HotkeyController` trait, with two impls that are **never** used together (the windowed app and headless backend are mutually exclusive — both bind :47474): @@ -768,7 +770,7 @@ one new alias entry + one new test case in `parse-log.spec.ts`. `bundle.active: false` in `tauri.conf.json` — overlay:build skips MSI and NSIS, just produces the bare `.exe` at -`apps/desktop/src-tauri/target/release/tarkov-checker-desktop.exe`. +`apps/desktop/src-tauri/target/release/raidmate.exe`. Flip to `true` (with `bundle.targets: ["msi","nsis"]` or `"all"`) when a release flow needs installers. @@ -812,7 +814,7 @@ DeviceGuard').SecurityServicesRunning` → should be empty/`0`. VBS 4. **The repo must live on an ASCII path.** `cargo metadata` segfaults on a Cyrillic CWD (which is what made the OneDrive Documents path — `C:\Users\\OneDrive\Документы\...` — unusable). Current location - is `C:\git-repos\tarkov-checker`. If you ever clone fresh into a + is `C:\git-repos\raidmate`. If you ever clone fresh into a Cyrillic path, either move it or `mklink /J` an ASCII alias and run pnpm from there. The legacy junction `C:\tarkov-checker` is no longer required and can be removed with `rmdir C:\tarkov-checker`. @@ -839,7 +841,7 @@ DeviceGuard').SecurityServicesRunning` → should be empty/`0`. VBS Resource step depends on it). The icon set (`32x32.png`, `128x128.png`, `128x128@2x.png`, `icon.ico`, `icon.icns`, the `Square*Logo`/`StoreLogo` set) is generated from `apps/client/public/favicon.svg` via - `pnpm --filter @tarkov-checker/desktop exec tauri icon /favicon.svg` + `pnpm --filter @raidmate/desktop exec tauri icon /favicon.svg` — the same mark the browser build uses. Re-run that after editing the SVG; the generator also emits `ios/` + `android/` dirs which we delete (this is a Windows-only desktop app). `icons/128x128.png` is also `include_bytes!`'d @@ -853,11 +855,11 @@ DeviceGuard').SecurityServicesRunning` → should be empty/`0`. VBS toast shows "PowerShell" + its icon). Fix: we don't use the plugin. `src/notify.rs` registers `HKCU\Software\Classes\AppUserModelId\` with a `DisplayName` + `IconUri` (the favicon PNG, written to - `%APPDATA%/tarkov-checker/notification-icon.png`) at startup, then emits the + `%APPDATA%/raidmate/notification-icon.png`) at startup, then emits the toast via `tauri-winrt-notification::Toast::new()`. No installer / Start-Menu shortcut needed. Only used today for the one-time "still running in the tray" hint (`commands::notify_tray_hint`, fired by the - client's `useCloseConfirm` gated on `tc.overlay.trayHintShown`). + client's `useCloseConfirm` gated on `rm.overlay.trayHintShown`). ## CI & releases @@ -882,7 +884,7 @@ git push origin v0.1.0 Pushing the tag triggers a fresh CI run from the tagged commit (~3-5 min warm cache, ~8-12 cold). On success `softprops/action-gh-release@v2` -publishes a GitHub Release named `v0.1.0` with `tarkov-checker-desktop.exe` +publishes a GitHub Release named `v0.1.0` with `raidmate.exe` attached and marked as **Latest**. End users grab it from the Releases tab; the .exe lives there indefinitely. diff --git a/CREDITS.md b/CREDITS.md index 8478931..daf9de1 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -11,7 +11,7 @@ Licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4 See [`apps/client/public/maps/LICENSE.md`](apps/client/public/maps/LICENSE.md) for the full text and the explicit no-cheating clause. -`tarkov-checker` is a personal, non-commercial tool — the NC clause is +`RaidMate` is a personal, non-commercial tool — the NC clause is satisfied. Should this repo ever be open-sourced, the SVG maps remain under their original CC BY-NC-SA 4.0 license; the submodule keeps them separate from this project's code so the two licenses don't entangle. diff --git a/README.md b/README.md index e9dd54f..64a1652 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# tarkov-checker +# RaidMate A live in-raid map companion for Escape from Tarkov. The overlay watches your Tarkov screenshots and logs in real time, tracking your position and marking extracts on an interactive Leaflet map with community SVG layers. @@ -25,27 +25,27 @@ A live in-raid map companion for Escape from Tarkov. The overlay watches your Ta ### End Users -1. Download `tarkov-checker-desktop.exe` from the [latest Release](../../releases/latest). +1. Download `raidmate.exe` from the [latest Release](../../releases/latest). 2. Drop it anywhere — Desktop, USB stick, Downloads folder. It's completely portable. 3. Double-click to launch. On first run, the overlay auto-detects your Tarkov installation via the Windows registry. If that fails, manually set the paths in **Settings → Tarkov paths**. 4. In Tarkov, press **PrintScreen** during a raid. The screenshot drops into your Tarkov screenshots folder, and the overlay reads its filename to update your position on the map. -**Data storage**: settings and cache live in `%APPDATA%/tarkov-checker/`. By default, closing the window exits the app. Optionally, enable "Minimize to tray" in Settings to hide the window to the system tray instead of closing (the tray icon's "Show" restores it, "Quit" exits). +**Data storage**: settings and cache live in `%APPDATA%/raidmate/`. By default, closing the window exits the app. Optionally, enable "Minimize to tray" in Settings to hide the window to the system tray instead of closing (the tray icon's "Show" restores it, "Quit" exits). ### Developers Clone with submodules, install dependencies, then choose your dev mode: ```bash -git clone --recurse-submodules https://github.com/Mosmain/tarkov-checker.git -cd tarkov-checker +git clone --recurse-submodules https://github.com/Mosmain/raidmate.git +cd raidmate pnpm install ``` **Dev** (one command — Tauri spawns Vite on :5173 and the helper on :47474): ```bash -pnpm --filter @tarkov-checker/desktop tauri:dev +pnpm --filter @raidmate/desktop tauri:dev ``` A browser on the same PC or a phone on the same Wi-Fi can hit @@ -63,7 +63,7 @@ The overlay needs two directories: Resolution priority (highest to lowest): 1. **Environment variables** — `TARKOV_GAME_DIR`, `TARKOV_SCREENSHOT_DIR` (optional; useful for non-standard setups) -2. **Manual override** — Settings → Tarkov paths, persisted to `%APPDATA%/tarkov-checker/config.json` +2. **Manual override** — Settings → Tarkov paths, persisted to `%APPDATA%/raidmate/config.json` 3. **Windows registry auto-detect** — the BSG launcher writes the install path to the registry, and Windows tracks the real Documents location even when redirected to OneDrive If auto-detect fails, fill in **Game folder** in Settings and click **Save**. Watchers restart immediately — no app restart needed. @@ -123,7 +123,7 @@ See [LICENSE](./LICENSE). --- -# tarkov-checker (русская версия) +# RaidMate (русская версия) Спутник-карта для рейдов в Escape from Tarkov. Оверлей следит за скриншотами и логами вашей игры, отслеживая вашу позицию в реальном времени и отмечая выходы на интерактивной Leaflet-карте с SVG-слоями от сообщества. @@ -150,27 +150,27 @@ See [LICENSE](./LICENSE). ### Для игроков -1. Скачай `tarkov-checker-desktop.exe` из [последнего релиза](../../releases/latest). +1. Скачай `raidmate.exe` из [последнего релиза](../../releases/latest). 2. Положи куда угодно — на Рабочий стол, флешку, папку Downloads. Полностью портативное приложение. 3. Запусти двойным кликом. При первом запуске оверлей автоматически обнаружит твою установку Tarkov через реестр Windows. Если не получится, вручную укажи пути в **Settings → Tarkov paths**. 4. В Tarkov нажми **PrintScreen** во время рейда. Скриншот упадёт в папку скриншотов Tarkov, а оверлей прочитает имя файла и обновит твою позицию на карте. -**Хранение данных**: настройки и кэш живут в `%APPDATA%/tarkov-checker/`. По умолчанию закрытие окна завершает приложение. Опционально включи "Minimize to tray" в Settings, чтобы скрыть окно в системный трей вместо закрытия (пункт "Show" в меню трея его восстанавливает, "Quit" завершает приложение). +**Хранение данных**: настройки и кэш живут в `%APPDATA%/raidmate/`. По умолчанию закрытие окна завершает приложение. Опционально включи "Minimize to tray" в Settings, чтобы скрыть окно в системный трей вместо закрытия (пункт "Show" в меню трея его восстанавливает, "Quit" завершает приложение). ### Для разработчиков Клонируй с сабмодулями, установи зависимости, затем выбери режим разработки: ```bash -git clone --recurse-submodules https://github.com/Mosmain/tarkov-checker.git -cd tarkov-checker +git clone --recurse-submodules https://github.com/Mosmain/raidmate.git +cd raidmate pnpm install ``` **Разработка десктоп-оверлея** (одна команда — Tauri поднимает Vite на :5173): ```bash -pnpm --filter @tarkov-checker/desktop tauri:dev +pnpm --filter @raidmate/desktop tauri:dev ``` Браузер на том же ПК или телефон в той же Wi-Fi могут открывать @@ -188,7 +188,7 @@ pnpm --filter @tarkov-checker/desktop tauri:dev Приоритет определения (от высшего к низшему): 1. **Переменные окружения** — `TARKOV_GAME_DIR`, `TARKOV_SCREENSHOT_DIR` (опционально; полезно для нестандартных установок) -2. **Ручное переопределение** — Settings → Tarkov paths, сохраняется в `%APPDATA%/tarkov-checker/config.json` +2. **Ручное переопределение** — Settings → Tarkov paths, сохраняется в `%APPDATA%/raidmate/config.json` 3. **Автодетект из реестра Windows** — лаунчер BSG пишет путь установки в реестр, и Windows отслеживает реальное расположение Документов даже когда они перенаправлены в OneDrive Если автодетект не сработал, заполни **Game folder** в Settings и нажми **Save**. Слежение перезапустится сразу — перезапуска приложения не требуется. diff --git a/apps/client/README.md b/apps/client/README.md index 646bd43..ffa9d85 100644 --- a/apps/client/README.md +++ b/apps/client/README.md @@ -1,4 +1,4 @@ -# @tarkov-checker/client +# @raidmate/client Vue + Leaflet map. Runs both inside the Tauri overlay (`apps/desktop`) and as a plain browser page on phones over LAN (the same prod build the in-process Rust helper in `apps/desktop` serves at `:47474/`; dev uses Vite on `:5173`, which proxies `/api` + `/events` to the helper). Same code path, different transport (Tauri events vs SSE / `EventSource`). @@ -85,7 +85,7 @@ For nested or dynamic routes (`/raids/[id]`), see [Vue Router file-based docs](h 1. Create `src/features//` with whichever subfolders apply: `store.ts`, `composables/`, `components/`, `api/`, `lib/`. 2. Inside the feature: `./X` and `../X` imports. 3. Other features depend on yours via `@/features//`. -4. If the feature has persisted user settings — create `store.ts` using [`persistedRef`](./src/shared/persisted-store.ts) from `@/shared/persisted-store`. Each setting gets its own key like `tc..`. +4. If the feature has persisted user settings — create `store.ts` using [`persistedRef`](./src/shared/persisted-store.ts) from `@/shared/persisted-store`. Each setting gets its own key like `rm..`. 5. If the feature has its own SYSTEM/app settings UI (the gear drawer). Map-layer settings live on the layer registration instead — see step 6. - Create a section component `XxxSection.vue` under [`features/settings/sections/`](./src/features/settings/sections/). diff --git a/apps/client/index.html b/apps/client/index.html index 5a97677..08e2624 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -12,8 +12,8 @@ - - tarkov-checker + + RaidMate
diff --git a/apps/client/package.json b/apps/client/package.json index 30e68e9..058fe46 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,5 +1,5 @@ { - "name": "@tarkov-checker/client", + "name": "@raidmate/client", "version": "0.0.0", "private": true, "type": "module", @@ -16,7 +16,7 @@ }, "dependencies": { "@primeuix/themes": "^2.0.3", - "@tarkov-checker/shared": "workspace:*", + "@raidmate/shared": "workspace:*", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-global-shortcut": "^2.3.1", "@tauri-apps/plugin-window-state": "^2.4.1", diff --git a/apps/client/public/manifest.webmanifest b/apps/client/public/manifest.webmanifest index 0421aaa..7ae3d06 100644 --- a/apps/client/public/manifest.webmanifest +++ b/apps/client/public/manifest.webmanifest @@ -1,6 +1,6 @@ { - "name": "tarkov-checker", - "short_name": "tarkov-checker", + "name": "RaidMate", + "short_name": "RaidMate", "description": "Live Escape from Tarkov map overlay — second-screen map for your phone.", "start_url": "/", "scope": "/", diff --git a/apps/client/src/__tests__/hotkeys-store.spec.ts b/apps/client/src/__tests__/hotkeys-store.spec.ts index 90ed9f7..3b6af95 100644 --- a/apps/client/src/__tests__/hotkeys-store.spec.ts +++ b/apps/client/src/__tests__/hotkeys-store.spec.ts @@ -67,27 +67,4 @@ describe('useHotkeysStore — backend sync', () => { await store.setAction('zoomIn', 'CommandOrControl+Alt+Q'); expect(store.zoomInHotkey).toBe(DEFAULTS.zoomIn); // reverted }); - - it('migrates a customised legacy localStorage combo once', async () => { - // Old per-field persistedRef stored JSON-encoded strings. - localStorage.setItem('tc.hotkeys.zoomIn', JSON.stringify('CommandOrControl+Alt+M')); - fetchHotkeysMock.mockResolvedValue(DEFAULTS); - putHotkeysMock.mockResolvedValue({ ...DEFAULTS, zoomIn: 'CommandOrControl+Alt+M' }); - - const store = useHotkeysStore(); - await store.load(); - - expect(putHotkeysMock).toHaveBeenCalledWith({ zoomIn: 'CommandOrControl+Alt+M' }); - expect(store.zoomInHotkey).toBe('CommandOrControl+Alt+M'); - }); - - it('does not migrate when legacy values equal the backend defaults', async () => { - localStorage.setItem('tc.hotkeys.zoomIn', JSON.stringify(DEFAULTS.zoomIn)); - fetchHotkeysMock.mockResolvedValue(DEFAULTS); - - const store = useHotkeysStore(); - await store.load(); - - expect(putHotkeysMock).not.toHaveBeenCalled(); - }); }); diff --git a/apps/client/src/__tests__/persisted-store.spec.ts b/apps/client/src/__tests__/persisted-store.spec.ts index f675baf..216ef5d 100644 --- a/apps/client/src/__tests__/persisted-store.spec.ts +++ b/apps/client/src/__tests__/persisted-store.spec.ts @@ -12,61 +12,61 @@ describe('persistedRef', () => { }); it('returns the default value when nothing is persisted', () => { - const r = persistedRef('tc.test.empty', enumSchema, 'a'); + const r = persistedRef('rm.test.empty', enumSchema, 'a'); expect(r.value).toBe('a'); }); it('round-trips a value through localStorage', async () => { - const a = persistedRef('tc.test.roundtrip', enumSchema, 'a'); + const a = persistedRef('rm.test.roundtrip', enumSchema, 'a'); a.value = 'b'; await nextTick(); - expect(localStorage.getItem('tc.test.roundtrip')).toBe('"b"'); + expect(localStorage.getItem('rm.test.roundtrip')).toBe('"b"'); - const b = persistedRef('tc.test.roundtrip', enumSchema, 'a'); + const b = persistedRef('rm.test.roundtrip', enumSchema, 'a'); expect(b.value).toBe('b'); }); it('falls back to default on corrupt JSON without throwing', () => { - localStorage.setItem('tc.test.corrupt', '{not valid json'); - const r = persistedRef('tc.test.corrupt', enumSchema, 'c'); + localStorage.setItem('rm.test.corrupt', '{not valid json'); + const r = persistedRef('rm.test.corrupt', enumSchema, 'c'); expect(r.value).toBe('c'); }); it('leaves the corrupt entry in place so a future schema can recover it', () => { - localStorage.setItem('tc.test.corrupt-keep', '{not valid json'); - persistedRef('tc.test.corrupt-keep', enumSchema, 'c'); - expect(localStorage.getItem('tc.test.corrupt-keep')).toBe('{not valid json'); + localStorage.setItem('rm.test.corrupt-keep', '{not valid json'); + persistedRef('rm.test.corrupt-keep', enumSchema, 'c'); + expect(localStorage.getItem('rm.test.corrupt-keep')).toBe('{not valid json'); }); it('falls back to default on schema mismatch', () => { - localStorage.setItem('tc.test.mismatch', '"not-in-enum"'); - const r = persistedRef('tc.test.mismatch', enumSchema, 'a'); + localStorage.setItem('rm.test.mismatch', '"not-in-enum"'); + const r = persistedRef('rm.test.mismatch', enumSchema, 'a'); expect(r.value).toBe('a'); }); it('falls back to default when a number outranges its schema', () => { - localStorage.setItem('tc.test.range', '999'); - const r = persistedRef('tc.test.range', numberSchema, 42); + localStorage.setItem('rm.test.range', '999'); + const r = persistedRef('rm.test.range', numberSchema, 42); expect(r.value).toBe(42); }); it('keeps keys isolated — writing one does not touch another', async () => { - const left = persistedRef('tc.test.left', enumSchema, 'a'); - const right = persistedRef('tc.test.right', enumSchema, 'a'); + const left = persistedRef('rm.test.left', enumSchema, 'a'); + const right = persistedRef('rm.test.right', enumSchema, 'a'); left.value = 'b'; right.value = 'c'; await nextTick(); - expect(localStorage.getItem('tc.test.left')).toBe('"b"'); - expect(localStorage.getItem('tc.test.right')).toBe('"c"'); + expect(localStorage.getItem('rm.test.left')).toBe('"b"'); + expect(localStorage.getItem('rm.test.right')).toBe('"c"'); }); it('persists nested objects through deep watch', async () => { const objSchema = z.object({ count: z.number(), tags: z.array(z.string()) }); - const r = persistedRef('tc.test.nested', objSchema, { count: 0, tags: [] }); + const r = persistedRef('rm.test.nested', objSchema, { count: 0, tags: [] }); r.value.count = 5; r.value.tags.push('one'); await nextTick(); - const stored: unknown = JSON.parse(localStorage.getItem('tc.test.nested') ?? 'null'); + const stored: unknown = JSON.parse(localStorage.getItem('rm.test.nested') ?? 'null'); expect(stored).toEqual({ count: 5, tags: ['one'] }); }); }); diff --git a/apps/client/src/__tests__/useLayerVisibility.spec.ts b/apps/client/src/__tests__/useLayerVisibility.spec.ts index 9829cd7..48990be 100644 --- a/apps/client/src/__tests__/useLayerVisibility.spec.ts +++ b/apps/client/src/__tests__/useLayerVisibility.spec.ts @@ -16,15 +16,15 @@ describe('useLayerVisibility', () => { expect(useLayerVisibility('test-singleton')).toBe(useLayerVisibility('test-singleton')); }); - it('persists a toggle to tc.layer..visible', async () => { + it('persists a toggle to rm.layer..visible', async () => { const v = useLayerVisibility('test-persist'); v.value = false; await nextTick(); - expect(localStorage.getItem('tc.layer.test-persist.visible')).toBe('false'); + expect(localStorage.getItem('rm.layer.test-persist.visible')).toBe('false'); }); it('reads an existing persisted value on first use', () => { - localStorage.setItem('tc.layer.test-seeded.visible', 'false'); + localStorage.setItem('rm.layer.test-seeded.visible', 'false'); expect(useLayerVisibility('test-seeded').value).toBe(false); }); }); diff --git a/apps/client/src/app/router.ts b/apps/client/src/app/router.ts index 055cb52..2597492 100644 --- a/apps/client/src/app/router.ts +++ b/apps/client/src/app/router.ts @@ -14,7 +14,7 @@ import { isTauri } from '@/shared/tauri'; * window is re-opened. The browser build keeps web history so deep links * (e.g. /raid on a phone) survive bookmarking. * - * `import.meta.env.BASE_URL` (`/` for local builds, `/tarkov-checker/` for + * `import.meta.env.BASE_URL` (`/` for local builds, `/raidmate/` for * the GitHub Pages project page — see `vite.config.ts`) prefixes every * route so deep links land under the right subpath. */ diff --git a/apps/client/src/features/airdrop/store.ts b/apps/client/src/features/airdrop/store.ts index 24d2278..5582011 100644 --- a/apps/client/src/features/airdrop/store.ts +++ b/apps/client/src/features/airdrop/store.ts @@ -79,7 +79,7 @@ export const useAirdropStore = defineStore('airdrop', () => { // banner ticks down the remaining distance as they approach. const playerCurrentPos = ref<{ readonly x: number; readonly z: number } | null>(null); const dropMarkerRadius = persistedRef( - 'tc.airdrop.dropMarkerRadius', + 'rm.airdrop.dropMarkerRadius', dropMarkerRadiusSchema, DROP_MARKER_RADIUS_DEFAULT, ); diff --git a/apps/client/src/features/hotkeys/store.ts b/apps/client/src/features/hotkeys/store.ts index f4a2740..fc07ce9 100644 --- a/apps/client/src/features/hotkeys/store.ts +++ b/apps/client/src/features/hotkeys/store.ts @@ -1,10 +1,9 @@ -import { z } from 'zod'; import { persistedRef } from '@/shared/persisted-store'; -import { acceleratorSchema, type HotkeyConfig, type HotkeyPatch } from '@shared/hotkeys-api'; +import { acceleratorSchema, type HotkeyConfig } from '@shared/hotkeys-api'; import { fetchHotkeys, putHotkeys } from '@/features/server/api/hotkeys-api'; // Default combos — must match the Rust `HotkeyConfig::default()` so an -// un-customised install agrees on both ends and the migration below is a no-op. +// un-customised install agrees on both ends. const DEFAULTS: HotkeyConfig = { zoomIn: 'CommandOrControl+=', zoomOut: 'CommandOrControl+-', @@ -13,28 +12,6 @@ const DEFAULTS: HotkeyConfig = { airdrop: 'CommandOrControl+Alt+D', }; -// localStorage keys the old client-owned store used (per-field persistedRefs). -const LEGACY_KEYS: Record = { - zoomIn: 'tc.hotkeys.zoomIn', - zoomOut: 'tc.hotkeys.zoomOut', - floorUp: 'tc.hotkeys.floorUp', - floorDown: 'tc.hotkeys.floorDown', - airdrop: 'tc.hotkeys.airdrop', -}; - -/** Read a legacy persistedRef value (stored as JSON) and validate it. */ -function readLegacy(key: string): string | null { - if (typeof localStorage === 'undefined') return null; - const raw = localStorage.getItem(key); - if (raw === null) return null; - try { - const parsed = acceleratorSchema.safeParse(JSON.parse(raw)); - return parsed.success ? parsed.data : null; - } catch { - return null; - } -} - /** * Hotkey settings store. The overlay **lock** combo stays client-owned (it * toggles click-through — a window op, overlay-only — and is registered via @@ -44,7 +21,7 @@ function readLegacy(key: string): string | null { * start at the defaults so reads before `load()` are safe. */ export const useHotkeysStore = defineStore('hotkeys', () => { - const lockHotkey = persistedRef('tc.hotkeys.lock', acceleratorSchema, 'CommandOrControl+Alt+L'); + const lockHotkey = persistedRef('rm.hotkeys.lock', acceleratorSchema, 'CommandOrControl+Alt+L'); const zoomInHotkey = ref(DEFAULTS.zoomIn); const zoomOutHotkey = ref(DEFAULTS.zoomOut); @@ -66,32 +43,10 @@ export const useHotkeysStore = defineStore('hotkeys', () => { } } - // One-time migration: if the user customised combos in the old localStorage - // store, push those up to the backend the first time the new client runs. - // Defaults match, so an un-customised install never PUTs anything. - const migrated = persistedRef('tc.hotkeys.migrated', z.boolean(), false); - - async function migrateLegacy(backend: HotkeyConfig): Promise { - if (migrated.value) return; - migrated.value = true; - const patch: HotkeyPatch = {}; - for (const key of Object.keys(LEGACY_KEYS) as (keyof HotkeyConfig)[]) { - const legacy = readLegacy(LEGACY_KEYS[key]); - if (legacy && legacy !== backend[key]) { - patch[key] = legacy; - } - } - if (Object.keys(patch).length > 0) { - applyConfig(await putHotkeys(patch)); - } - } - - /** Load combos from the backend and run the one-time legacy migration. + /** Load combos from the backend. * Mounted once at the app root (see `useHotkeysSync`). */ async function load(): Promise { - const backend = await fetchHotkeys(); - applyConfig(backend); - await migrateLegacy(backend); + applyConfig(await fetchHotkeys()); } /** Persist a single action's combo. Optimistically updates the local ref, diff --git a/apps/client/src/features/i18n/locales/en.json b/apps/client/src/features/i18n/locales/en.json index 9c4be7e..0531ebf 100644 --- a/apps/client/src/features/i18n/locales/en.json +++ b/apps/client/src/features/i18n/locales/en.json @@ -53,18 +53,12 @@ "mobileHint": "Open this page on the machine running Tarkov to configure paths.", "deleteScreenshots": "Delete screenshots after reading", "deleteScreenshotsHint": "Sends each Tarkov screenshot to the Recycle Bin once its position has been read. Keeps the folder from filling up. Off by default.", - "source": { - "env": "from .env", - "manual": "manual", - "detected": "auto-detected", - "missing": "not found" - }, "missingTooltip": "Folder doesn't exist on disk yet.", "placeholderGameDir": "D:\\EFT", "placeholderScreenshotsDir": "Documents\\Escape from Tarkov\\Screenshots", "errors": { "noHelperTitle": "The helper isn't running", - "noHelperBody": "Start tarkov-checker.exe on the same machine as Tarkov to configure or sync game folders here. Refresh this page once the helper is up.", + "noHelperBody": "Start raidmate.exe on the same machine as Tarkov to configure or sync game folders here. Refresh this page once the helper is up.", "otherTitle": "Couldn't load Tarkov paths" } }, @@ -72,7 +66,12 @@ "available": "Update v{version} available", "install": "Update", "installing": "Updating…", - "error": "Update failed. Grab the new exe manually: GitHub → Releases." + "error": "Update failed. Grab the new exe manually: GitHub → Releases.", + "checkButton": "Check for updates", + "upToDate": "Up to date", + "checkFailed": "Check failed", + "updateTo": "Update to v{version}", + "autoCheckLabel": "Check for updates on startup" }, "overlay": { "heading": "Overlay", @@ -134,7 +133,7 @@ }, "trayHint": { "title": "Still running", - "body": "tarkov-checker is now in the system tray. Open it from there, or right-click the tray icon to quit." + "body": "RaidMate is now in the system tray. Open it from there, or right-click the tray icon to quit." }, "airdrop": { "armedFirst": "Drop tracker armed while facing the airdrop and press PrintScreen", @@ -149,14 +148,14 @@ "radiusHint": "How big the predicted-landing circle is on the map. Two-shot triangulation isn't perfectly precise — the circle just makes the uncertainty honest. Adjust to taste." }, "tray": { - "lock": "Lock (click-through)", + "lock": "Lock overlay (clicks pass to game)", "showWindow": "Show window", "alwaysOnTop": "Always on top", - "playerFollow": "Player follow", + "updateAvailable": "Update available: v{version}", "pairPhone": "Pair phone", "copyUrl": "Copy LAN URL", "quit": "Quit", - "tooltip": "tarkov-checker — running" + "tooltip": "RaidMate — running" }, "extractNames": { "bigmap": { diff --git a/apps/client/src/features/i18n/locales/ru.json b/apps/client/src/features/i18n/locales/ru.json index 1a1df13..e524c8b 100644 --- a/apps/client/src/features/i18n/locales/ru.json +++ b/apps/client/src/features/i18n/locales/ru.json @@ -53,18 +53,12 @@ "mobileHint": "Открой эту страницу на машине с Tarkov, чтобы настроить пути.", "deleteScreenshots": "Удалять скриншоты после считывания", "deleteScreenshotsHint": "Отправляет каждый скриншот Tarkov в корзину после считывания позиции. Не даёт папке разрастаться. По умолчанию выключено.", - "source": { - "env": "из .env", - "manual": "вручную", - "detected": "найдено автоматически", - "missing": "не найдено" - }, "missingTooltip": "Папка пока не существует.", "placeholderGameDir": "D:\\EFT", "placeholderScreenshotsDir": "Documents\\Escape from Tarkov\\Screenshots", "errors": { "noHelperTitle": "Хелпер не запущен", - "noHelperBody": "Запусти tarkov-checker.exe на той же машине что и Tarkov, чтобы настроить или синхронизировать папки игры. После запуска хелпера обнови эту страницу.", + "noHelperBody": "Запусти raidmate.exe на той же машине что и Tarkov, чтобы настроить или синхронизировать папки игры. После запуска хелпера обнови эту страницу.", "otherTitle": "Не удалось загрузить пути Tarkov" } }, @@ -72,7 +66,12 @@ "available": "Доступно обновление v{version}", "install": "Обновить", "installing": "Обновляем…", - "error": "Не получилось обновиться. Скачай новый exe вручную: GitHub → Releases." + "error": "Не получилось обновиться. Скачай новый exe вручную: GitHub → Releases.", + "checkButton": "Проверить обновления", + "upToDate": "Актуальная версия", + "checkFailed": "Не удалось проверить", + "updateTo": "Обновить до v{version}", + "autoCheckLabel": "Проверять обновления при запуске" }, "overlay": { "heading": "Оверлей", @@ -134,7 +133,7 @@ }, "trayHint": { "title": "Работает в фоне", - "body": "tarkov-checker свёрнут в системный трей. Открой его оттуда или нажми правый клик по иконке, чтобы выйти." + "body": "RaidMate свёрнут в системный трей. Открой его оттуда или нажми правый клик по иконке, чтобы выйти." }, "airdrop": { "armedFirst": "Drop-трекер запущен, смотри на дроп и нажми PrintScreen", @@ -149,14 +148,14 @@ "radiusHint": "Размер круга предполагаемого места падения на карте. Триангуляция по двум снимкам не идеально точная — круг просто честно показывает погрешность. Подбери на свой вкус." }, "tray": { - "lock": "Блокировка (клики сквозь)", + "lock": "Блокировка оверлея (клики идут в игру)", "showWindow": "Показать окно", "alwaysOnTop": "Поверх всех окон", - "playerFollow": "Авто-следование", + "updateAvailable": "Доступно обновление: v{version}", "pairPhone": "Подключить телефон", "copyUrl": "Копировать LAN-ссылку", "quit": "Выход", - "tooltip": "tarkov-checker — работает" + "tooltip": "RaidMate — работает" }, "extractNames": { "bigmap": { diff --git a/apps/client/src/features/i18n/store.ts b/apps/client/src/features/i18n/store.ts index 5be7b2e..6e5b154 100644 --- a/apps/client/src/features/i18n/store.ts +++ b/apps/client/src/features/i18n/store.ts @@ -15,7 +15,7 @@ function detectInitialLang(): ApiLang { } export const useI18nStore = defineStore('i18n', () => { - const apiLang = persistedRef('tc.i18n.apiLang', apiLangSchema, detectInitialLang()); + const apiLang = persistedRef('rm.i18n.apiLang', apiLangSchema, detectInitialLang()); // Mirror the persisted apiLang into the vue-i18n instance: on first read // (initial value), and on every subsequent change. `setLocale` is lazy — diff --git a/apps/client/src/features/map/composables/useLayerVisibility.ts b/apps/client/src/features/map/composables/useLayerVisibility.ts index 4d5604b..819a869 100644 --- a/apps/client/src/features/map/composables/useLayerVisibility.ts +++ b/apps/client/src/features/map/composables/useLayerVisibility.ts @@ -18,7 +18,7 @@ export function useLayerVisibility(id: string): Ref { if (cached) return cached; // scope.run only returns undefined if the scope is stopped; ours never is. const r = scope.run(() => - persistedRef(`tc.layer.${id}.visible`, z.boolean(), true), + persistedRef(`rm.layer.${id}.visible`, z.boolean(), true), ) as Ref; refs.set(id, r); return r; diff --git a/apps/client/src/features/map/composables/useLeafletMap.ts b/apps/client/src/features/map/composables/useLeafletMap.ts index 4ce8d81..f3b0588 100644 --- a/apps/client/src/features/map/composables/useLeafletMap.ts +++ b/apps/client/src/features/map/composables/useLeafletMap.ts @@ -166,7 +166,7 @@ export function useLeafletMap( async function loadSvgIntoMap(instance: LeafletMap): Promise { try { mapError.value = null; - // BASE_URL is `/` for local/Tauri builds and `/tarkov-checker/` on + // BASE_URL is `/` for local/Tauri builds and `/raidmate/` on // GitHub Pages — runtime fetches need the prefix manually since // Vite only rewrites build-time asset references in HTML/CSS. const svgUrl = mapSvgPath(mapCode, `${import.meta.env.BASE_URL}maps`); diff --git a/apps/client/src/features/map/layers/extracts/icon.ts b/apps/client/src/features/map/layers/extracts/icon.ts index bedcacb..7a9a2db 100644 --- a/apps/client/src/features/map/layers/extracts/icon.ts +++ b/apps/client/src/features/map/layers/extracts/icon.ts @@ -6,7 +6,7 @@ export const EXTRACT_ICON_SIZE = 26; /** Canonical render order — keeps composite slices stable across reorderings. */ export const FACTION_ORDER: ReadonlyArray = ['pmc', 'scav', 'shared']; -// `BASE_URL` is `/` for local/Tauri builds and `/tarkov-checker/` on +// `BASE_URL` is `/` for local/Tauri builds and `/raidmate/` on // GitHub Pages — runtime URLs in HTML strings need the prefix manually // since Vite only rewrites build-time asset references in template/CSS. // Same pattern as `mapSvgPath(...)` in useLeafletMap.ts. diff --git a/apps/client/src/features/map/store.ts b/apps/client/src/features/map/store.ts index 30f2f54..fa94bc0 100644 --- a/apps/client/src/features/map/store.ts +++ b/apps/client/src/features/map/store.ts @@ -17,28 +17,28 @@ export type ExtractLabelSize = z.infer; const DEFAULT_FACTIONS: readonly ExtractFactionFilter[] = ['pmc', 'scav', 'shared']; export const useMapSettingsStore = defineStore('map-settings', () => { - const mapCode = persistedRef('tc.map.code', mapCodeSchema, 'bigmap' as TarkovMapCode); - const extractFactions = persistedRef('tc.map.extractFactions', z.array(extractFactionSchema), [ + const mapCode = persistedRef('rm.map.code', mapCodeSchema, 'bigmap' as TarkovMapCode); + const extractFactions = persistedRef('rm.map.extractFactions', z.array(extractFactionSchema), [ ...DEFAULT_FACTIONS, ]); const extractLabelMode = persistedRef( - 'tc.map.extractLabelMode', + 'rm.map.extractLabelMode', labelModeSchema, 'always' as ExtractLabelMode, ); const extractLabelSize = persistedRef( - 'tc.map.extractLabelSize', + 'rm.map.extractLabelSize', labelSizeSchema, 'md' as ExtractLabelSize, ); // Off-screen extract arrows on the viewport edge. Opt-in, off by default. - const edgeIndicators = persistedRef('tc.map.edgeIndicators', z.boolean(), false); + const edgeIndicators = persistedRef('rm.map.edgeIndicators', z.boolean(), false); const playerFollow = persistedRef( - 'tc.map.playerFollow', + 'rm.map.playerFollow', playerFollowSchema, 'off' as PlayerFollow, ); - const autoMapSwitch = persistedRef('tc.map.autoMapSwitch', autoMapSwitchSchema, true); + const autoMapSwitch = persistedRef('rm.map.autoMapSwitch', autoMapSwitchSchema, true); return { mapCode, diff --git a/apps/client/src/features/overlay/composables/useTrayIcon.ts b/apps/client/src/features/overlay/composables/useTrayIcon.ts index 58aead6..d7e401f 100644 --- a/apps/client/src/features/overlay/composables/useTrayIcon.ts +++ b/apps/client/src/features/overlay/composables/useTrayIcon.ts @@ -1,20 +1,11 @@ import type { TrayIcon, TrayIconEvent } from '@tauri-apps/api/tray'; import type { Menu, CheckMenuItem } from '@tauri-apps/api/menu'; -import { useI18nStore } from '@/features/i18n/store'; import { useOverlayStore } from '@/features/overlay/store'; -import { useMapSettingsStore } from '@/features/map/store'; +import { useUpdaterStore } from '@/features/updater/store'; type TrayHandle = Awaited>; -const TRAY_ID = 'tarkov-checker-tray'; - -async function showWindow(): Promise { - const { getCurrentWindow } = await import('@tauri-apps/api/window'); - const win = getCurrentWindow(); - await win.show(); - await win.unminimize(); - await win.setFocus(); -} +const TRAY_ID = 'raidmate-tray'; /** * Owns the Tauri system-tray icon lifecycle: creates the icon on mount, @@ -24,19 +15,18 @@ async function showWindow(): Promise { * * The tray is the only control surface reachable while the game is fullscreen * (the overlay can be hidden or click-through-locked behind it), so the menu - * leans on actions that matter mid-game: unlock, restore, the always-on-top / - * player-follow toggles, and the LAN-share shortcuts. Left-click restores the - * window (Windows convention); right-click opens the menu. Since ✕ now parks - * the overlay in the tray (see overlay store `minimizeToTray`), "Quit" here is - * the canonical way to actually exit. + * holds window/session-level rescues and lifecycle only; map-layer settings + * live on the LayerRail. Left-click restores the window (Windows convention); + * right-click opens the menu. Since ✕ parks the overlay in the tray (see + * overlay store `minimizeToTray`), "Quit" here is the canonical way to exit. */ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref): void { if (!isTauri) return; - const { t } = useI18n(); - const { apiLang } = storeToRefs(useI18nStore()); + const { t, locale } = useI18n(); const { alwaysOnTop, pairingModalOpen } = storeToRefs(useOverlayStore()); - const { playerFollow } = storeToRefs(useMapSettingsStore()); + const updaterStore = useUpdaterStore(); + const { info: updateInfo, bannerDismissed } = storeToRefs(updaterStore); let trayRef: TrayHandle | null = null; // Live references to the check items so external state changes (hotkey, @@ -44,7 +34,27 @@ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref) // rebuilding the whole menu. let lockItem: CheckMenuItem | null = null; let aotItem: CheckMenuItem | null = null; - let followItem: CheckMenuItem | null = null; + + async function showWindow(): Promise { + try { + const { getCurrentWindow } = await import('@tauri-apps/api/window'); + const win = getCurrentWindow(); + await win.show(); + await win.unminimize(); + // A visible-but-covered window won't raise on setFocus() alone: + // Windows' foreground lock lets SetForegroundWindow fail silently for + // background processes. Pulse always-on-top to force the raise, then + // restore the user's setting. + await win.setAlwaysOnTop(true); + await win.setFocus(); + if (!alwaysOnTop.value) await win.setAlwaysOnTop(false); + } catch (err) { + // Most likely a missing core:window:allow-* capability — surface it, + // these rejections are otherwise invisible (see CLAUDE.md). + // eslint-disable-next-line no-console + console.error('[tray] showWindow failed:', err); + } + } async function copyLanUrl(): Promise { try { @@ -57,13 +67,29 @@ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref) } async function buildTrayMenu(): Promise { + // Snapshot every label BEFORE the first await: the locale can flip + // mid-build (it's set asynchronously by the i18n store), and a build + // interleaved with that flip produces a mixed-language menu. + const labels = { + lock: t('tray.lock'), + show: t('tray.showWindow'), + aot: t('tray.alwaysOnTop'), + update: updateInfo.value + ? t('tray.updateAvailable', { version: updateInfo.value.latest }) + : null, + pair: t('tray.pairPhone'), + copy: t('tray.copyUrl'), + quit: t('tray.quit'), + tooltip: t('tray.tooltip'), + }; + const { Menu, MenuItem, CheckMenuItem, PredefinedMenuItem } = await import('@tauri-apps/api/menu'); const separator = () => PredefinedMenuItem.new({ item: 'Separator' }); lockItem = await CheckMenuItem.new({ id: 'toggle-lock', - text: t('tray.lock'), + text: labels.lock, checked: overlayClickThrough.value, action: () => { overlayClickThrough.value = !overlayClickThrough.value; @@ -71,28 +97,20 @@ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref) }); const showItem = await MenuItem.new({ id: 'show', - text: t('tray.showWindow'), + text: labels.show, action: () => void showWindow(), }); aotItem = await CheckMenuItem.new({ id: 'always-on-top', - text: t('tray.alwaysOnTop'), + text: labels.aot, checked: alwaysOnTop.value, action: () => { alwaysOnTop.value = !alwaysOnTop.value; }, }); - followItem = await CheckMenuItem.new({ - id: 'player-follow', - text: t('tray.playerFollow'), - checked: playerFollow.value === 'on', - action: () => { - playerFollow.value = playerFollow.value === 'on' ? 'off' : 'on'; - }, - }); const pairItem = await MenuItem.new({ id: 'pair-phone', - text: t('tray.pairPhone'), + text: labels.pair, action: () => { void showWindow(); pairingModalOpen.value = true; @@ -100,12 +118,12 @@ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref) }); const copyItem = await MenuItem.new({ id: 'copy-url', - text: t('tray.copyUrl'), + text: labels.copy, action: () => void copyLanUrl(), }); const quitItem = await MenuItem.new({ id: 'quit', - text: t('tray.quit'), + text: labels.quit, action: () => void (async () => { const { getCurrentWindow } = await import('@tauri-apps/api/window'); @@ -113,14 +131,28 @@ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref) })(), }); + // Shows the window + banner instead of installing: install respawns the + // app — too destructive for a stray tray click. + const updateItem = labels.update + ? await MenuItem.new({ + id: 'update-available', + text: labels.update, + action: () => { + bannerDismissed.value = false; + void showWindow(); + }, + }) + : null; + + // Three groups: overlay-mode toggles / window presence / LAN sharing, + // plus Quit at arm's length (irreversible among recoverables). return Menu.new({ items: [ lockItem, + aotItem, await separator(), showItem, - await separator(), - aotItem, - followItem, + ...(updateItem ? [updateItem] : []), await separator(), pairItem, copyItem, @@ -162,16 +194,22 @@ export function useTrayIcon(isTauri: boolean, overlayClickThrough: Ref) // Keep checkmarks in sync when state is changed from anywhere else. watch(overlayClickThrough, (v) => void lockItem?.setChecked(v)); watch(alwaysOnTop, (v) => void aotItem?.setChecked(v)); - watch(playerFollow, (v) => void followItem?.setChecked(v === 'on')); - watch(apiLang, async () => { + // Full rebuild on language change AND when an update appears/clears — + // the conditional "update available" item can't be toggled in place. + // Watches the actual vue-i18n `locale` (what t() reads), NOT the store's + // apiLang: the store applies apiLang to the locale asynchronously, so an + // apiLang watcher races the flip — it both misses the startup apply (tray + // builds before the persisted language lands, screenshot: RU app / EN tray) + // and rebuilds too early on a switch. + watch([locale, updateInfo], async () => { if (!trayRef) return; try { await trayRef.setMenu(await buildTrayMenu()); await trayRef.setTooltip(t('tray.tooltip')); } catch (err) { // eslint-disable-next-line no-console - console.error('[tray] i18n refresh failed:', err); + console.error('[tray] menu refresh failed:', err); } }); diff --git a/apps/client/src/features/overlay/store.ts b/apps/client/src/features/overlay/store.ts index 89ab505..320ccc6 100644 --- a/apps/client/src/features/overlay/store.ts +++ b/apps/client/src/features/overlay/store.ts @@ -5,22 +5,22 @@ const overlayZoomSchema = z.enum(['75', '100', '125', '150']); export type OverlayZoom = z.infer; export const useOverlayStore = defineStore('overlay', () => { - const alwaysOnTop = persistedRef('tc.overlay.alwaysOnTop', z.boolean(), false); - const opacity = persistedRef('tc.overlay.opacity', z.number().min(0.3).max(1), 1); - const mapOpacity = persistedRef('tc.overlay.mapOpacity', z.number().min(0).max(1), 1); - const zoom = persistedRef('tc.overlay.zoom', overlayZoomSchema, '100' as OverlayZoom); + const alwaysOnTop = persistedRef('rm.overlay.alwaysOnTop', z.boolean(), false); + const opacity = persistedRef('rm.overlay.opacity', z.number().min(0.3).max(1), 1); + const mapOpacity = persistedRef('rm.overlay.mapOpacity', z.number().min(0).max(1), 1); + const zoom = persistedRef('rm.overlay.zoom', overlayZoomSchema, '100' as OverlayZoom); // When on, the ✕ button hides the window to the system tray (keeps running) // instead of quitting; restore via the tray's "Show window", quit via the // tray's "Quit". Default ON — closing parks the overlay in the tray rather // than killing the session. Users who prefer ✕ = confirmed quit can flip it // off in Settings. - const minimizeToTray = persistedRef('tc.overlay.minimizeToTray', z.boolean(), true); + const minimizeToTray = persistedRef('rm.overlay.minimizeToTray', z.boolean(), true); // One-shot: tracks whether the "still running in the tray" notification has // been shown. Set true after the first close-to-tray so the hint fires once, // not on every ✕. - const trayHintShown = persistedRef('tc.overlay.trayHintShown', z.boolean(), false); + const trayHintShown = persistedRef('rm.overlay.trayHintShown', z.boolean(), false); // Session-only: booting into a locked overlay with a broken hotkey would be // unrecoverable, so the locked state is intentionally NOT persisted. diff --git a/apps/client/src/features/server/composables/useServerPaths.ts b/apps/client/src/features/server/composables/useServerPaths.ts index 901903c..68bebe1 100644 --- a/apps/client/src/features/server/composables/useServerPaths.ts +++ b/apps/client/src/features/server/composables/useServerPaths.ts @@ -6,7 +6,7 @@ export type PathSlot = 'gameDir' | 'screenshotsDir' | 'logsDir'; /** * Typed UI-state for path-API failures. `no-helper` is the common case * when the SPA runs in a browser (hosted or LAN-phone) and the user has - * not started `tarkov-checker.exe` yet — the panel renders a friendly + * not started `raidmate.exe` yet — the panel renders a friendly * "start the helper" message instead of the raw fetch error. */ export type PathsError = { kind: 'no-helper' } | { kind: 'other'; detail: string }; diff --git a/apps/client/src/features/settings/SettingsPanel.vue b/apps/client/src/features/settings/SettingsPanel.vue index 4f3f065..66eb7df 100644 --- a/apps/client/src/features/settings/SettingsPanel.vue +++ b/apps/client/src/features/settings/SettingsPanel.vue @@ -2,6 +2,7 @@ import { z } from 'zod'; import { useSettingsSections } from './registry'; import { persistedRef } from '@/shared/persisted-store'; +import AboutFooter from '@/features/updater/components/AboutFooter.vue'; const { t } = useI18n(); const systemSections = useSettingsSections(); @@ -13,7 +14,7 @@ const isDesktop = useMediaQuery('(min-width: 640px)'); // paths/pairing). Desktop opens every section; the overlay/phone start // collapsed to keep the sheet short. Snapshotted once, then user choices persist. const openSections = persistedRef( - 'tc.settings.open', + 'rm.settings.open', z.array(z.string()), isDesktop.value ? systemSections.value.map((s) => s.id) : [], ); @@ -67,5 +68,6 @@ const openSections = persistedRef( + diff --git a/apps/client/src/features/updater/components/AboutFooter.vue b/apps/client/src/features/updater/components/AboutFooter.vue new file mode 100644 index 0000000..a4ed2f2 --- /dev/null +++ b/apps/client/src/features/updater/components/AboutFooter.vue @@ -0,0 +1,85 @@ + + + diff --git a/apps/client/src/features/updater/components/UpdateBanner.vue b/apps/client/src/features/updater/components/UpdateBanner.vue index f35b2b4..75b4773 100644 --- a/apps/client/src/features/updater/components/UpdateBanner.vue +++ b/apps/client/src/features/updater/components/UpdateBanner.vue @@ -1,71 +1,39 @@