From 33cbc937067f93b763f35df524d165ef9c240037 Mon Sep 17 00:00:00 2001 From: Mosmain Date: Thu, 28 May 2026 03:26:29 +0300 Subject: [PATCH 1/5] refactor(client): streamline API calls and enhance overlay components - Introduced a new transport layer for API calls, consolidating Tauri and HTTP requests into a single function for better maintainability. - Refactored server configuration and Tarkov extracts fetching to utilize the new transport mechanism, improving code clarity and reducing duplication. - Added new overlay components for error handling and header display, enhancing user feedback and interaction within the Tauri overlay. - Simplified opacity handling in the MapQuickMenu and SettingsPanel components, improving the overall user experience with cleaner code and better state management. --- apps/client/src/App.vue | 262 ++--------- apps/client/src/api/server-config.ts | 50 +-- apps/client/src/api/tarkov-dev.ts | 31 +- apps/client/src/api/transport.ts | 66 +++ apps/client/src/components/MapQuickMenu.vue | 18 +- apps/client/src/components/SettingsPanel.vue | 262 ++--------- .../src/components/overlay/OverlayErrors.vue | 24 + .../src/components/overlay/OverlayHeader.vue | 68 +++ .../overlay/OverlayLockIndicator.vue | 45 ++ .../client/src/composables/useCloseConfirm.ts | 32 ++ .../src/composables/useExtractMarkers.ts | 215 +++++++++ .../composables/useExtractsCacheControl.ts | 98 +++++ .../src/composables/useFloorSwitcher.ts | 96 ++++ apps/client/src/composables/useLeafletMap.ts | 414 ++---------------- .../src/composables/useOverlayBootstrap.ts | 24 + apps/client/src/composables/useOverlaySync.ts | 57 +++ .../client/src/composables/usePlayerMarker.ts | 102 +++++ apps/client/src/composables/useServerPaths.ts | 128 ++++++ .../src/composables/useServerTransport.ts | 23 +- apps/client/src/composables/useTrayIcon.ts | 95 ++++ apps/client/src/stores/raid.ts | 23 - apps/client/src/utils/opacity.ts | 20 + 22 files changed, 1209 insertions(+), 944 deletions(-) create mode 100644 apps/client/src/api/transport.ts create mode 100644 apps/client/src/components/overlay/OverlayErrors.vue create mode 100644 apps/client/src/components/overlay/OverlayHeader.vue create mode 100644 apps/client/src/components/overlay/OverlayLockIndicator.vue create mode 100644 apps/client/src/composables/useCloseConfirm.ts create mode 100644 apps/client/src/composables/useExtractMarkers.ts create mode 100644 apps/client/src/composables/useExtractsCacheControl.ts create mode 100644 apps/client/src/composables/useFloorSwitcher.ts create mode 100644 apps/client/src/composables/useOverlayBootstrap.ts create mode 100644 apps/client/src/composables/useOverlaySync.ts create mode 100644 apps/client/src/composables/usePlayerMarker.ts create mode 100644 apps/client/src/composables/useServerPaths.ts create mode 100644 apps/client/src/composables/useTrayIcon.ts delete mode 100644 apps/client/src/stores/raid.ts create mode 100644 apps/client/src/utils/opacity.ts diff --git a/apps/client/src/App.vue b/apps/client/src/App.vue index 0bf89b5..9885380 100644 --- a/apps/client/src/App.vue +++ b/apps/client/src/App.vue @@ -1,25 +1,24 @@ diff --git a/apps/client/src/api/server-config.ts b/apps/client/src/api/server-config.ts index f847485..5aad2ef 100644 --- a/apps/client/src/api/server-config.ts +++ b/apps/client/src/api/server-config.ts @@ -3,46 +3,22 @@ import { type ServerConfigResponse, type ServerConfigUpdate, } from "@shared/config-api"; +import { callBackend } from "./transport"; -const isTauri = "__TAURI_INTERNALS__" in window; +const parseConfig = (d: unknown): ServerConfigResponse => serverConfigResponseSchema.parse(d); -function apiBase(): string { - return `http://${window.location.hostname}:3000`; -} - -async function tauriInvoke(name: string, args?: Record): Promise { - // Lazy-import so the chunk only loads when actually running inside Tauri. - const { invoke } = await import("@tauri-apps/api/core"); - return invoke(name, args); -} - -export async function fetchServerConfig(): Promise { - if (isTauri) { - const data = await tauriInvoke("get_config"); - return serverConfigResponseSchema.parse(data); - } - const r = await fetch(`${apiBase()}/api/config`); - if (!r.ok) throw new Error(`GET /api/config failed: HTTP ${r.status}`); - return serverConfigResponseSchema.parse(await r.json()); +export function fetchServerConfig(): Promise { + return callBackend({ + tauri: { cmd: "get_config" }, + http: { method: "GET", path: "/api/config" }, + parse: parseConfig, + }); } -export async function putServerConfig( - patch: ServerConfigUpdate, -): Promise { - if (isTauri) { - const data = await tauriInvoke("update_config", { patch }); - return serverConfigResponseSchema.parse(data); - } - const r = await fetch(`${apiBase()}/api/config`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(patch), +export function putServerConfig(patch: ServerConfigUpdate): Promise { + return callBackend({ + tauri: { cmd: "update_config", args: { patch } }, + http: { method: "PUT", path: "/api/config", body: patch }, + parse: parseConfig, }); - if (!r.ok) { - const body: unknown = await r.json().catch(() => ({})); - const error = - typeof body === "object" && body !== null && "error" in body ? body.error : body; - throw new Error(`PUT /api/config failed: HTTP ${r.status} — ${JSON.stringify(error)}`); - } - return serverConfigResponseSchema.parse(await r.json()); } diff --git a/apps/client/src/api/tarkov-dev.ts b/apps/client/src/api/tarkov-dev.ts index 1f03f3c..1b55bb8 100644 --- a/apps/client/src/api/tarkov-dev.ts +++ b/apps/client/src/api/tarkov-dev.ts @@ -3,34 +3,21 @@ import { type ExtractsCacheResponse, type MapExtracts, } from "@shared/tarkov-api"; +import { callBackend } from "./transport"; export type ApiLang = "en" | "ru" | "de" | "fr" | "es" | "it" | "ja" | "pl" | "pt" | "zh"; -const isTauri = "__TAURI_INTERNALS__" in window; - -function apiBase(): string { - return `http://${window.location.hostname}:3000`; -} - const fetchedAtByLang = new Map(); const inFlight = new Map>(); -async function requestExtracts(lang: ApiLang, refresh: boolean): Promise { - if (isTauri) { - const { invoke } = await import("@tauri-apps/api/core"); - const data = await invoke("get_extracts", { lang, refresh }); - return extractsCacheResponse.parse(data); - } - const params = new URLSearchParams({ lang }); - if (refresh) params.set("refresh", "1"); - const r = await fetch(`${apiBase()}/api/extracts?${params.toString()}`); - if (!r.ok) { - const body: unknown = await r.json().catch(() => ({})); - const detail = - typeof body === "object" && body !== null && "error" in body ? body.error : body; - throw new Error(`/api/extracts failed: HTTP ${r.status} — ${JSON.stringify(detail)}`); - } - return extractsCacheResponse.parse(await r.json()); +function requestExtracts(lang: ApiLang, refresh: boolean): Promise { + const query: Record = { lang }; + if (refresh) query.refresh = "1"; + return callBackend({ + tauri: { cmd: "get_extracts", args: { lang, refresh } }, + http: { method: "GET", path: "/api/extracts", query }, + parse: (d) => extractsCacheResponse.parse(d), + }); } /** diff --git a/apps/client/src/api/transport.ts b/apps/client/src/api/transport.ts new file mode 100644 index 0000000..93ab803 --- /dev/null +++ b/apps/client/src/api/transport.ts @@ -0,0 +1,66 @@ +/** + * Single dispatch point for client→backend reads/writes. The same client code + * runs both inside the Tauri overlay and as a plain browser PWA: + * + * - In Tauri: invokes a named IPC command via `@tauri-apps/api/core`. + * - In a plain browser: calls the LAN Node server (apps/server) at :3000 + * with the given HTTP method/path/body/query. + * + * The raw response is fed through `parse` (typically a zod schema's `.parse`) + * so callers get a validated, typed result either way. + */ + +const isTauri = "__TAURI_INTERNALS__" in window; + +function apiBase(): string { + return `http://${window.location.hostname}:3000`; +} + +export interface TauriCall { + cmd: string; + args?: Record; +} + +export interface HttpCall { + method?: "GET" | "PUT" | "POST" | "DELETE"; + path: string; + body?: unknown; + query?: Record; +} + +export interface BackendCall { + tauri: TauriCall; + http: HttpCall; + parse: (data: unknown) => T; +} + +export async function callBackend(call: BackendCall): Promise { + if (isTauri) { + // Lazy-import so the @tauri-apps/api chunk only loads when actually + // running inside Tauri. + const { invoke } = await import("@tauri-apps/api/core"); + const data = await invoke(call.tauri.cmd, call.tauri.args); + return call.parse(data); + } + return httpRequest(call.http, call.parse); +} + +async function httpRequest(http: HttpCall, parse: (data: unknown) => T): Promise { + const method = http.method ?? "GET"; + const qs = http.query ? "?" + new URLSearchParams(http.query).toString() : ""; + const init: RequestInit = { method }; + if (http.body !== undefined) { + init.headers = { "Content-Type": "application/json" }; + init.body = JSON.stringify(http.body); + } + const r = await fetch(`${apiBase()}${http.path}${qs}`, init); + if (!r.ok) { + const body: unknown = await r.json().catch(() => ({})); + const detail = + typeof body === "object" && body !== null && "error" in body ? body.error : body; + throw new Error( + `${method} ${http.path} failed: HTTP ${r.status} — ${JSON.stringify(detail)}`, + ); + } + return parse(await r.json()); +} diff --git a/apps/client/src/components/MapQuickMenu.vue b/apps/client/src/components/MapQuickMenu.vue index d7acf9e..c21560b 100644 --- a/apps/client/src/components/MapQuickMenu.vue +++ b/apps/client/src/components/MapQuickMenu.vue @@ -5,6 +5,7 @@ import { onClickOutside, useEventListener } from "@vueuse/core"; import Slider from "primevue/slider"; import { useSettingsStore } from "../stores/settings"; import { useUiText } from "../i18n"; +import { opacityPercentBinding } from "../utils/opacity"; const settings = useSettingsStore(); const { overlayOpacity, overlayMapOpacity } = storeToRefs(settings); @@ -13,21 +14,8 @@ const t = useUiText(); const position = ref<{ x: number; y: number } | null>(null); const panelRef = ref(null); -// Slider %-bindings reuse the same conversion as SettingsPanel. -const opacityPercent = computed({ - get: () => Math.round(overlayOpacity.value * 100), - set: (pct) => { - overlayOpacity.value = Math.max(30, Math.min(100, pct)) / 100; - }, -}); - -const mapOpacityPercent = computed({ - get: () => Math.round(overlayMapOpacity.value * 100), - set: (pct) => { - overlayMapOpacity.value = Math.max(0, Math.min(100, pct)) / 100; - }, -}); - +const opacityPercent = opacityPercentBinding(overlayOpacity, 30, 100); +const mapOpacityPercent = opacityPercentBinding(overlayMapOpacity, 0, 100); const mapOpacityDisabled = computed(() => overlayOpacity.value >= 1); /** Panel dimensions used to clamp the opening position to the viewport. The diff --git a/apps/client/src/components/SettingsPanel.vue b/apps/client/src/components/SettingsPanel.vue index 12cbf25..dba3c10 100644 --- a/apps/client/src/components/SettingsPanel.vue +++ b/apps/client/src/components/SettingsPanel.vue @@ -1,6 +1,7 @@