diff --git a/web/src/components/SettingsMenu.tsx b/web/src/components/SettingsMenu.tsx index a855508c..603eb9d8 100644 --- a/web/src/components/SettingsMenu.tsx +++ b/web/src/components/SettingsMenu.tsx @@ -4,9 +4,10 @@ */ import React, { useState } from "react"; -import { Cog6ToothIcon, BellIcon, ClockIcon } from "@heroicons/react/24/outline"; +import { Cog6ToothIcon, BellIcon, ClockIcon, MapPinIcon } from "@heroicons/react/24/outline"; import { NotificationSettings } from "./settings/NotificationSettings"; import { TimeFormatSettings } from "./settings/TimeFormatSettings"; +import { DistanceSettings } from "./settings/DistanceSettings"; import { Button } from "@/components/ui/Button"; import { Dialog, @@ -41,6 +42,12 @@ const settingsSections: SettingsSection[] = [ icon: , component: TimeFormatSettings, }, + { + id: "distance", + label: "Distance Units", + icon: , + component: DistanceSettings, + }, ]; export const SettingsMenu: React.FC = () => { diff --git a/web/src/components/settings/DistanceSettings.tsx b/web/src/components/settings/DistanceSettings.tsx new file mode 100644 index 00000000..e6e8a248 --- /dev/null +++ b/web/src/components/settings/DistanceSettings.tsx @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024-2025, s0up and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import React, { useState } from "react"; +import { MapPinIcon, CheckIcon } from "@heroicons/react/24/outline"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/Button"; +import { + getDistanceSettings, + saveDistanceSettings, + formatDistance, + type DistanceSettings as DistanceSettingsType, + type DistanceUnit, +} from "@/utils/distanceSettings"; +import { showToast } from "@/components/common/Toast"; + +export const DistanceSettings: React.FC = () => { + const [settings, setSettings] = useState(getDistanceSettings); + const [hasChanges, setHasChanges] = useState(false); + + const updateUnit = (unit: DistanceUnit) => { + setSettings({ unit }); + setHasChanges(true); + }; + + const saveSettings = () => { + saveDistanceSettings(settings); + setHasChanges(false); + showToast("Distance settings saved", "success", { + description: "Changes will be applied across the application", + }); + }; + + const resetToDefaults = () => { + setSettings({ unit: "km" }); + setHasChanges(true); + }; + + return ( +
+
+
+

+ + Distance Settings +

+

+ Choose between metric and imperial units for distance display +

+
+ + {hasChanges && ( +
+ + +
+ )} +
+ +
+ + + + + Distance Unit + + + +
+ + +
+ +
+
+ +
+
+ Preview +
+
+ 100 km = {formatDistance(100, settings)} +
+
+
+
+
+
+
+ + + +
+
+

+ Quick Actions +

+

+ Reset settings or apply common configurations +

+
+
+ +
+
+
+
+
+ ); +}; diff --git a/web/src/components/speedtest/ServerList.tsx b/web/src/components/speedtest/ServerList.tsx index eef95984..8357958f 100644 --- a/web/src/components/speedtest/ServerList.tsx +++ b/web/src/components/speedtest/ServerList.tsx @@ -18,6 +18,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; +import { formatDistance, useDistanceSettings } from "@/utils/distanceSettings"; import { Select, SelectContent, @@ -92,6 +93,7 @@ export const ServerList: React.FC = ({ const saved = localStorage.getItem("server-list-open"); return saved === null ? true : saved === "true"; }); + const { settings: distanceSettings } = useDistanceSettings(); // Persist server list open state to localStorage useEffect(() => { @@ -560,9 +562,7 @@ export const ServerList: React.FC = ({
{filteredServersWithSelect .slice(0, displayCount) - .map((server) => { - const distance = server.distance; - return ( + .map((server) => ( = ({ {server.country} -{" "} - {Math.floor(distance)} km + {formatDistance(server.distance, distanceSettings)}
- ); - })} + ))} )} diff --git a/web/src/components/speedtest/traceroute/components/ServerCard.tsx b/web/src/components/speedtest/traceroute/components/ServerCard.tsx index 87178074..292d88b9 100644 --- a/web/src/components/speedtest/traceroute/components/ServerCard.tsx +++ b/web/src/components/speedtest/traceroute/components/ServerCard.tsx @@ -11,6 +11,7 @@ import { getServerTypeLabel, getServerTypeColorClass, } from "../utils/serverUtils"; +import { formatDistance, useDistanceSettings } from "@/utils/distanceSettings"; interface ServerCardProps { server: Server; @@ -25,6 +26,8 @@ export const ServerCard: React.FC = ({ onSelect, disabled = false, }) => { + const { settings: distanceSettings } = useDistanceSettings(); + return ( = ({ {server.isIperf ? "Custom Server" - : `${server.country} - ${Math.floor(server.distance)} km`} + : `${server.country} - ${formatDistance(server.distance, distanceSettings)}`} {getServerTypeLabel(server)} diff --git a/web/src/utils/distanceSettings.ts b/web/src/utils/distanceSettings.ts new file mode 100644 index 00000000..bfe0e02e --- /dev/null +++ b/web/src/utils/distanceSettings.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024-2025, s0up and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import React from "react"; + +export type DistanceUnit = "km" | "mi"; + +export interface DistanceSettings { + unit: DistanceUnit; +} + +const DISTANCE_SETTINGS_KEY = "netronome-distance-settings"; +const DEFAULT_SETTINGS: DistanceSettings = { + unit: "km", +}; + +const KM_TO_MI = 0.621371; + +export const getDistanceSettings = (): DistanceSettings => { + try { + const saved = localStorage.getItem(DISTANCE_SETTINGS_KEY); + if (saved) { + const settings = JSON.parse(saved); + return { + unit: settings.unit === "mi" ? "mi" : "km", + }; + } + } catch (error) { + console.warn("Failed to load distance settings from localStorage:", error); + } + return DEFAULT_SETTINGS; +}; + +export const saveDistanceSettings = (settings: DistanceSettings): void => { + try { + localStorage.setItem(DISTANCE_SETTINGS_KEY, JSON.stringify(settings)); + window.dispatchEvent(new CustomEvent("distanceSettingsChanged", { detail: settings })); + } catch (error) { + console.warn("Failed to save distance settings to localStorage:", error); + } +}; + +export const formatDistance = (distanceKm: number, settings?: DistanceSettings): string => { + const currentSettings = settings || getDistanceSettings(); + if (currentSettings.unit === "mi") { + return `${Math.round(distanceKm * KM_TO_MI)} mi`; + } + return `${Math.round(distanceKm)} km`; +}; + +export const useDistanceSettings = (): { settings: DistanceSettings; updateSettings: (newSettings: DistanceSettings) => void } => { + const [settings, setSettings] = React.useState(getDistanceSettings); + + React.useEffect(() => { + const handleSettingsChange = (event: CustomEvent) => { + setSettings(event.detail); + }; + + window.addEventListener("distanceSettingsChanged", handleSettingsChange as EventListener); + + return () => { + window.removeEventListener("distanceSettingsChanged", handleSettingsChange as EventListener); + }; + }, []); + + const updateSettings = (newSettings: DistanceSettings) => { + saveDistanceSettings(newSettings); + setSettings(newSettings); + }; + + return { settings, updateSettings }; +};