v0.1.3: interface scales with the window, realmlist line per Friends mode, robust close handler

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ДмитрийandClaude Sonnet 5.5 committed 2026-09-30 19:42:05 +05:00
1 parent 0e1016e93c
commit 4eda03ab63
12 files changed
+80 -23

No files matched your search

Generated
+3 -3
View File
@@ -347,7 +347,7 @@ dependencies = [
[[package]]
name = "coa-core"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -375,7 +375,7 @@ dependencies = [
[[package]]
name = "coa-release"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"chrono",
"coa-core",
@@ -384,7 +384,7 @@ dependencies = [
[[package]]
name = "coa-server-manager"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"coa-core",
"serde",
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/coa-core", "crates/coa-release", "src-tauri"]
[workspace.package]
version = "0.1.2"
version = "0.1.3"
edition = "2021"
license = "AGPL-3.0-only"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "coa-server-manager",
"private": true,
"version": "0.1.2",
"version": "0.1.3",
"license": "AGPL-3.0-only",
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CoA Server Manager",
"version": "0.1.2",
"version": "0.1.3",
"identifier": "com.coa.servermanager",
"build": {
"beforeDevCommand": "npm run dev",
+3
View File
@@ -430,6 +430,9 @@ const de: Partial<Record<keyof typeof en, string>> = {
"selfupd.now": "Jetzt installieren",
"about.auto": "Updates im Hintergrund laden und beim Schließen des Managers installieren",
"about.autoHint": "Ein Manager-Update stoppt oder verändert deine Server nie.",
// Friends line
"fr.copyLine": "Zeile kopieren",
};
export default de;
+3
View File
@@ -430,6 +430,9 @@ const en = {
"selfupd.now": "Install now",
"about.auto": "Download updates in the background and install them when I close the Manager",
"about.autoHint": "Your servers are never stopped or changed by a Manager update.",
// Friends line
"fr.copyLine": "Copy line",
} as const;
export default en;
+3
View File
@@ -430,6 +430,9 @@ const es: Partial<Record<keyof typeof en, string>> = {
"selfupd.now": "Instalar ahora",
"about.auto": "Descargar las actualizaciones en segundo plano e instalarlas al cerrar el Manager",
"about.autoHint": "Una actualización del Manager nunca detiene ni cambia tus servidores.",
// Friends line
"fr.copyLine": "Copiar línea",
};
export default es;
+3
View File
@@ -430,6 +430,9 @@ const fr: Partial<Record<keyof typeof en, string>> = {
"selfupd.now": "Installer maintenant",
"about.auto": "Télécharger les mises à jour en arrière-plan et les installer à la fermeture du Manager",
"about.autoHint": "Une mise à jour du Manager n’arrête ni ne modifie jamais vos serveurs.",
// Friends line
"fr.copyLine": "Copier la ligne",
};
export default fr;
+3
View File
@@ -430,6 +430,9 @@ const ru: Partial<Record<keyof typeof en, string>> = {
"selfupd.now": "Установить сейчас",
"about.auto": "Скачивать обновления в фоне и устанавливать их, когда я закрою менеджер",
"about.autoHint": "Обновление менеджера никогда не останавливает и не меняет ваши серверы.",
// Friends line
"fr.copyLine": "Скопировать строку",
};
export default ru;
+6
View File
@@ -16,6 +16,12 @@
--font-sans: "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
}
/* Everything is sized in rem, so scaling the root font grows the whole interface with the window
(16px up to about 1280px wide, then gradually larger; capped at 26px). */
html {
font-size: clamp(16px, calc(0.75vw + 6.4px), 26px);
}
html,
body,
#root {
+21 -16
View File
@@ -103,22 +103,27 @@ export async function installNow(): Promise<void> {
/** Install a downloaded update when the user closes the window. Call once at start-up. */
export function installOnClose(): () => void {
let stop: (() => void) | undefined;
void getCurrentWindow()
.onCloseRequested(async (event) => {
if (state.phase !== "ready" || !update || !autoUpdateEnabled()) return;
event.preventDefault();
try {
set({ phase: "installing" });
await update.install();
} catch (e) {
set({ phase: "error", error: String(e) });
await getCurrentWindow().destroy();
}
})
.then((un) => {
stop = un;
})
.catch(() => {});
try {
const win = getCurrentWindow();
void win
.onCloseRequested(async (event) => {
if (state.phase !== "ready" || !update || !autoUpdateEnabled()) return;
event.preventDefault();
try {
set({ phase: "installing" });
await update.install();
} catch (e) {
set({ phase: "error", error: String(e) });
await win.destroy();
}
})
.then((un) => {
stop = un;
})
.catch(() => {});
} catch {
/* not running inside the desktop shell (browser preview) */
}
return () => stop?.();
}
+32 -1
View File
@@ -22,6 +22,30 @@ const MODES: { id: FriendsMode; title: Key; text: Key }[] = [
{ id: "private", title: "fr.mode.private.title", text: "fr.mode.private.text" },
];
/** A ready-to-paste realmlist line for the friend, shown as soon as the address is known. */
function RealmlistLine({ host }: { host: string }) {
const t = useT();
const [copied, setCopied] = useState(false);
const line = `set realmlist ${host}`;
return (
<div className="mt-2 flex flex-wrap items-center gap-2">
<code className="selectable rounded bg-black/40 px-2 py-1 text-sm">{line}</code>
<Button
size="sm"
variant="ghost"
onClick={() =>
void navigator.clipboard.writeText(line).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
}
>
{copied ? t("fr.copied") : t("fr.copyLine")}
</Button>
</div>
);
}
export function FriendsPage({ serverId }: { serverId: string }) {
const t = useT();
const human = useHuman();
@@ -103,7 +127,12 @@ export function FriendsPage({ serverId }: { serverId: string }) {
</h3>
<p className="mt-0.5 text-sm text-muted">{t(m.text)}</p>
{m.id === "lan" && <p className="mt-2 text-sm">{t("fr.yourLan")} <b className="selectable">{st.lan_ip ?? t("fr.unknown")}</b></p>}
{m.id === "lan" && (
<>
<p className="mt-2 text-sm">{t("fr.yourLan")} <b className="selectable">{st.lan_ip ?? t("fr.unknown")}</b></p>
{st.lan_ip && cur.mode !== m.id && <RealmlistLine host={st.lan_ip} />}
</>
)}
{m.id === "direct" && (
<div className="mt-2 text-sm">
@@ -119,6 +148,7 @@ export function FriendsPage({ serverId }: { serverId: string }) {
{net?.public_ip && net.reachability !== "cgnat" && (
<p className="mt-2">{t("fr.publicAddr")} <b className="selectable">{net.public_ip}</b>{!net.router_found && <span className="text-muted">{t("fr.noRouter")}</span>}</p>
)}
{net?.public_ip && net.reachability !== "cgnat" && cur.mode !== m.id && <RealmlistLine host={net.public_ip} />}
{net && !net.public_ip && <p className="mt-2 text-warn">{t("fr.noInternet")}</p>}
</div>
)}
@@ -130,6 +160,7 @@ export function FriendsPage({ serverId }: { serverId: string }) {
{st.tailscale.connected && <>{t("fr.privateAddr")} <b className="selectable">{st.tailscale.ip}</b></>}
</p>
)}
{m.id === "private" && st.tailscale.connected && st.tailscale.ip && cur.mode !== m.id && <RealmlistLine host={st.tailscale.ip} />}
</div>
<Button
size="sm"