diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 73d8ea9df..ea9dbfea6 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -1982,6 +1982,8 @@ "wizard": { "title": "Link to a Termix server", "reloginTitle": "Sign in again", + "savedServers": "Recent servers", + "removeServer": "Remove from the list", "serverDescription": "Enter the address you use to open Termix in your browser.", "signinDescription": "Sign in to {{url}}", "chooseDescription": "This device already has data. What should happen to it?", diff --git a/src/ui/settings/sync/LinkServerDialog.tsx b/src/ui/settings/sync/LinkServerDialog.tsx index 4df6244b3..a656a17bf 100644 --- a/src/ui/settings/sync/LinkServerDialog.tsx +++ b/src/ui/settings/sync/LinkServerDialog.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { AlertCircle, Loader2 } from "lucide-react"; +import { AlertCircle, Loader2, X } from "lucide-react"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { Switch } from "@/components/switch"; @@ -23,6 +23,11 @@ import { type SyncStatus, } from "@/api/sync-api"; import { ProxySettingsFields } from "./ProxySettingsFields"; +import { + forgetServerUrl, + getSavedServerUrls, + rememberServerUrl, +} from "./saved-servers"; type Step = "server" | "signin" | "choose" | "finishing"; @@ -69,6 +74,7 @@ export function LinkServerDialog({ }: LinkServerDialogProps) { const { t } = useTranslation(); const [step, setStep] = useState(relogin ? "signin" : "server"); + const [savedUrls, setSavedUrls] = useState(getSavedServerUrls); const [target, setTarget] = useState({ serverUrl: relogin?.serverUrl ?? "", customHeaders: [], @@ -157,6 +163,7 @@ export function LinkServerDialog({ mode: chosen, serverName, }); + rememberServerUrl(target.serverUrl); toast.success(t("sync.wizard.linked")); onLinked(status); onOpenChange(false); @@ -268,6 +275,35 @@ export function LinkServerDialog({

{t("sync.wizard.serverUrlHint")}

+ {!relogin && savedUrls.length > 0 && ( +
+ + {t("sync.wizard.savedServers")} + + {savedUrls.map((url) => ( +
+ + +
+ ))} +
+ )} {problem && ( diff --git a/src/ui/settings/sync/saved-servers.ts b/src/ui/settings/sync/saved-servers.ts new file mode 100644 index 000000000..3ed9e0a35 --- /dev/null +++ b/src/ui/settings/sync/saved-servers.ts @@ -0,0 +1,39 @@ +// Same key as 2.8, so a desktop keeps the servers it already used. +const SAVED_URLS_KEY = "termix_saved_server_urls"; +const MAX_SAVED_URLS = 5; + +export function getSavedServerUrls(): string[] { + try { + const parsed = JSON.parse(localStorage.getItem(SAVED_URLS_KEY) ?? "[]"); + return Array.isArray(parsed) + ? parsed.filter((url): url is string => typeof url === "string") + : []; + } catch { + return []; + } +} + +function write(urls: string[]): void { + try { + localStorage.setItem(SAVED_URLS_KEY, JSON.stringify(urls)); + } catch { + // Storage can be unavailable. + } +} + +export function rememberServerUrl(url: string): string[] { + const trimmed = url.trim().replace(/\/+$/, ""); + if (!trimmed) return getSavedServerUrls(); + const urls = [ + trimmed, + ...getSavedServerUrls().filter((saved) => saved !== trimmed), + ].slice(0, MAX_SAVED_URLS); + write(urls); + return urls; +} + +export function forgetServerUrl(url: string): string[] { + const urls = getSavedServerUrls().filter((saved) => saved !== url); + write(urls); + return urls; +} diff --git a/src/ui/tests/settings/sync/saved-servers.test.ts b/src/ui/tests/settings/sync/saved-servers.test.ts new file mode 100644 index 000000000..02dcc888d --- /dev/null +++ b/src/ui/tests/settings/sync/saved-servers.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + forgetServerUrl, + getSavedServerUrls, + rememberServerUrl, +} from "@/settings/sync/saved-servers"; + +afterEach(() => localStorage.clear()); + +describe("saved server urls", () => { + it("keeps the newest five, without duplicates or trailing slashes", () => { + for (let i = 1; i <= 6; i++) rememberServerUrl(`https://s${i}.example/`); + rememberServerUrl("https://s4.example"); + expect(getSavedServerUrls()).toEqual([ + "https://s4.example", + "https://s6.example", + "https://s5.example", + "https://s3.example", + "https://s2.example", + ]); + }); + + it("forgets a server and survives bad storage", () => { + rememberServerUrl("https://a.example"); + expect(forgetServerUrl("https://a.example")).toEqual([]); + localStorage.setItem("termix_saved_server_urls", "{broken"); + expect(getSavedServerUrls()).toEqual([]); + }); +});