mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-13 04:21:02 +00:00
v0.1.1: language picker on welcome screen, friendly errors for unpublished package / unreachable server, backlog notes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68050882b8
commit
f620f87432
15 files changed
+102
-22
No files matched your search
Generated
+3
-3
@@ -347,7 +347,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -375,7 +375,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-release"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"coa-core",
|
||||
@@ -384,7 +384,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coa-server-manager"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"coa-core",
|
||||
"serde",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["crates/coa-core", "crates/coa-release", "src-tauri"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -20,6 +20,10 @@ pub enum Error {
|
||||
UnknownInstallation(String),
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
#[error("package not published: {0}")]
|
||||
PackageNotPublished(String),
|
||||
#[error("download server unreachable: {0}")]
|
||||
NetworkUnreachable(String),
|
||||
#[error("invalid values: {}", .0.iter().map(|f| format!("{}: {}", f.key, f.message)).collect::<Vec<_>>().join("; "))]
|
||||
Validation(Vec<FieldError>),
|
||||
}
|
||||
@@ -46,6 +50,8 @@ pub enum ErrorCode {
|
||||
StartupFailed,
|
||||
HashMismatch,
|
||||
PathRejected,
|
||||
PackageNotPublished,
|
||||
NetworkUnreachable,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -126,6 +132,16 @@ impl ErrorCode {
|
||||
"The requested file location is outside the server folder.",
|
||||
&[ShowDetails],
|
||||
),
|
||||
ErrorCode::PackageNotPublished => (
|
||||
"This server package is not available yet",
|
||||
"Nothing has been published at the download location. Try again later, or install from a local package folder (Advanced).",
|
||||
&[Retry, ShowDetails],
|
||||
),
|
||||
ErrorCode::NetworkUnreachable => (
|
||||
"Could not reach the download server",
|
||||
"Check your internet connection and try again.",
|
||||
&[Retry, ShowDetails],
|
||||
),
|
||||
ErrorCode::Unknown => ("Something went wrong", "See the technical details.", &[ShowDetails]),
|
||||
};
|
||||
Human { code: self, title, message, actions }
|
||||
@@ -139,6 +155,8 @@ impl Error {
|
||||
Error::HashMismatch { .. } => ErrorCode::HashMismatch,
|
||||
Error::PathRejected(_) => ErrorCode::PathRejected,
|
||||
Error::Validation(_) => ErrorCode::InvalidConfigValue,
|
||||
Error::PackageNotPublished(_) => ErrorCode::PackageNotPublished,
|
||||
Error::NetworkUnreachable(_) => ErrorCode::NetworkUnreachable,
|
||||
_ => ErrorCode::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ pub fn fetch_small(source: &Source, name: &str) -> Result<Vec<u8>> {
|
||||
let url = format!("{}/{name}", base.trim_end_matches('/'));
|
||||
download::check_url(&url)?;
|
||||
let t = download::HttpTransport::new()?;
|
||||
let reply = download::Transport::get(&t, &url, 0).map_err(|e| Error::Invalid(format!("Could not reach the download server: {e}")))?;
|
||||
let reply = download::Transport::get(&t, &url, 0).map_err(|e| Error::NetworkUnreachable(format!("{e}")))?;
|
||||
if reply.status == 404 || reply.status == 410 {
|
||||
return Err(Error::PackageNotPublished(format!("the download server answered {} for {name} ({url})", reply.status)));
|
||||
}
|
||||
if reply.status != 200 {
|
||||
return Err(Error::Invalid(format!("The download server answered {} for {name}.", reply.status)));
|
||||
}
|
||||
|
||||
@@ -483,3 +483,16 @@ The reference install classifies as **Healthy (customised)**: repack shape, bina
|
||||
`Intl`; settings-schema titles/descriptions (`schemas/*.json`) get per-locale overlays (`schemas/i18n/<locale>.json`);
|
||||
translations are reviewed by native speakers before release, with an English fallback for missing keys and a CI check
|
||||
that every locale has every key. Do this before the first public release so strings are not retro-fitted.
|
||||
|
||||
**Status (2026-09-30):** catalogue + picker + all screens translated (en/ru/de/fr/es drafts, `tools/check-i18n.mjs` checks
|
||||
keys/placeholders). Still English: settings-schema titles/descriptions/presets, companion size names, diagnostics check
|
||||
texts and update step names coming from the backend (need `schemas/i18n/<locale>.json` overlays and code-keyed backend strings).
|
||||
|
||||
* **Language choice on the very first screen** (requested 2026-09-30): the welcome screen must offer the language picker
|
||||
right away (before any other text is read), preselected from the OS language; the choice is persisted and shared with
|
||||
Settings. **Done 2026-09-30:** shared `LanguagePicker` (top-right of the welcome screen and in Settings).
|
||||
|
||||
* **Clean-install gap found by the owner (2026-09-30):** installing from the default URL fails with 404 because the `base`
|
||||
release does not exist yet (only `edge` is published). The message is now human ("package not available yet"). Also open:
|
||||
the base package has no `Data\` (maps/dbc/vmaps, ~3.8 GB); the installer needs a "use my existing Data folder / download"
|
||||
step, and the owner must decide hosting/legal for game data before `base` is published.
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coa-server-manager",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CoA Server Manager",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"identifier": "com.coa.servermanager",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Globe } from "lucide-react";
|
||||
import { LOCALES, useI18n, type Locale } from "@/i18n";
|
||||
|
||||
/** Language switch shared by the welcome screen and Settings. Names are shown in their own language. */
|
||||
export function LanguagePicker({ className }: { className?: string }) {
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
return (
|
||||
<label className={`inline-flex items-center gap-2 text-sm text-muted ${className ?? ""}`}>
|
||||
<Globe className="h-4 w-4" aria-hidden />
|
||||
<span className="sr-only">{t("settings.language")}</span>
|
||||
<select
|
||||
value={locale}
|
||||
onChange={(e) => setLocale(e.target.value as Locale)}
|
||||
className="cursor-pointer rounded-md border border-line bg-bg px-3 py-2 text-sm text-ink outline-none focus:border-gold"
|
||||
>
|
||||
{LOCALES.map((l) => (
|
||||
<option key={l.id} value={l.id}>
|
||||
{l.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -416,6 +416,12 @@ const de: Partial<Record<keyof typeof en, string>> = {
|
||||
"set.unsaved_other": "{n} nicht gespeicherte Änderungen",
|
||||
"set.discard": "Verwerfen",
|
||||
"set.save": "Änderungen speichern",
|
||||
|
||||
// Download errors
|
||||
"err.package_not_published.title": "Dieses Serverpaket ist noch nicht verfügbar",
|
||||
"err.package_not_published.message": "Am Download-Ort wurde noch nichts veröffentlicht. Versuche es später erneut oder installiere aus einem lokalen Paketordner (Erweitert).",
|
||||
"err.network_unreachable.title": "Der Download-Server war nicht erreichbar",
|
||||
"err.network_unreachable.message": "Prüfe deine Internetverbindung und versuche es erneut.",
|
||||
};
|
||||
|
||||
export default de;
|
||||
@@ -416,6 +416,12 @@ const en = {
|
||||
"set.unsaved_other": "{n} unsaved changes",
|
||||
"set.discard": "Discard",
|
||||
"set.save": "Save changes",
|
||||
|
||||
// Download errors
|
||||
"err.package_not_published.title": "This server package is not available yet",
|
||||
"err.package_not_published.message": "Nothing has been published at the download location. Try again later, or install from a local package folder (Advanced).",
|
||||
"err.network_unreachable.title": "Could not reach the download server",
|
||||
"err.network_unreachable.message": "Check your internet connection and try again.",
|
||||
} as const;
|
||||
|
||||
export default en;
|
||||
@@ -416,6 +416,12 @@ const es: Partial<Record<keyof typeof en, string>> = {
|
||||
"set.unsaved_other": "{n} cambios sin guardar",
|
||||
"set.discard": "Descartar",
|
||||
"set.save": "Guardar cambios",
|
||||
|
||||
// Download errors
|
||||
"err.package_not_published.title": "Este paquete del servidor aún no está disponible",
|
||||
"err.package_not_published.message": "Aún no se ha publicado nada en la ubicación de descarga. Inténtalo más tarde o instala desde una carpeta de paquete local (Avanzado).",
|
||||
"err.network_unreachable.title": "No se pudo contactar con el servidor de descargas",
|
||||
"err.network_unreachable.message": "Comprueba tu conexión a internet e inténtalo de nuevo.",
|
||||
};
|
||||
|
||||
export default es;
|
||||
@@ -416,6 +416,12 @@ const fr: Partial<Record<keyof typeof en, string>> = {
|
||||
"set.unsaved_other": "{n} modifications non enregistrées",
|
||||
"set.discard": "Annuler les modifications",
|
||||
"set.save": "Enregistrer les modifications",
|
||||
|
||||
// Download errors
|
||||
"err.package_not_published.title": "Ce paquet serveur n’est pas encore disponible",
|
||||
"err.package_not_published.message": "Rien n’a encore été publié à l’emplacement de téléchargement. Réessayez plus tard ou installez depuis un dossier de paquet local (Avancé).",
|
||||
"err.network_unreachable.title": "Impossible de joindre le serveur de téléchargement",
|
||||
"err.network_unreachable.message": "Vérifiez votre connexion internet et réessayez.",
|
||||
};
|
||||
|
||||
export default fr;
|
||||
@@ -416,6 +416,12 @@ const ru: Partial<Record<keyof typeof en, string>> = {
|
||||
"set.unsaved_other": "{n} несохранённых изменений",
|
||||
"set.discard": "Отменить",
|
||||
"set.save": "Сохранить изменения",
|
||||
|
||||
// Download errors
|
||||
"err.package_not_published.title": "Этот пакет сервера пока недоступен",
|
||||
"err.package_not_published.message": "В месте загрузки пока ничего не опубликовано. Повторите позже или установите из локальной папки с пакетом (Дополнительно).",
|
||||
"err.network_unreachable.title": "Не удалось связаться с сервером загрузки",
|
||||
"err.network_unreachable.message": "Проверьте подключение к интернету и повторите попытку.",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@@ -8,14 +8,15 @@ import { Card } from "@/components/ui/card";
|
||||
import { ClientCard } from "@/screens/ClientCard";
|
||||
import { DiagnosticsCard } from "@/screens/DiagnosticsCard";
|
||||
import { AboutCard } from "@/screens/AboutCard";
|
||||
import { LOCALES, useHuman, useI18n, type Key } from "@/i18n";
|
||||
import { useHuman, useI18n, type Key } from "@/i18n";
|
||||
import { LanguagePicker } from "@/components/LanguagePicker";
|
||||
|
||||
function mb(bytes: number) {
|
||||
return bytes >= 1 << 20 ? `${(bytes / (1 << 20)).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
||||
}
|
||||
|
||||
export function SettingsHome({ serverId }: { serverId: string }) {
|
||||
const { t, locale, setLocale } = useI18n();
|
||||
const { t } = useI18n();
|
||||
const human = useHuman();
|
||||
const [preview, setPreview] = useState<UpdatePreview | null>(null);
|
||||
const [pending, setPending] = useState<UpdateTxn | null>(null);
|
||||
@@ -111,18 +112,7 @@ export function SettingsHome({ serverId }: { serverId: string }) {
|
||||
<Card className="mt-6 p-6">
|
||||
<h2 className="font-semibold">{t("settings.language")}</h2>
|
||||
<p className="mt-1 text-sm text-muted">{t("settings.languageHint")}</p>
|
||||
<select
|
||||
aria-label={t("settings.language")}
|
||||
value={locale}
|
||||
onChange={(e) => setLocale(e.target.value as typeof locale)}
|
||||
className="mt-3 rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none focus:border-gold"
|
||||
>
|
||||
{LOCALES.map((l) => (
|
||||
<option key={l.id} value={l.id}>
|
||||
{l.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<LanguagePicker className="mt-3" />
|
||||
</Card>
|
||||
|
||||
<ClientCard serverId={serverId} />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { FolderSearch, Download } from "lucide-react";
|
||||
import { useT } from "@/i18n";
|
||||
import { LanguagePicker } from "@/components/LanguagePicker";
|
||||
|
||||
export function Welcome({ onImport, onInstall, onBack }: { onImport: () => void; onInstall: () => void; onBack?: () => void }) {
|
||||
const t = useT();
|
||||
return (
|
||||
<main className="flex h-full flex-col items-center justify-center px-8">
|
||||
<main className="relative flex h-full flex-col items-center justify-center px-8">
|
||||
<LanguagePicker className="absolute right-6 top-5" />
|
||||
<div className="mb-10 text-center">
|
||||
<div className="mx-auto mb-5 h-12 w-12 rounded-full border-2 border-gold/80 bg-card-2" aria-hidden />
|
||||
<h1 className="text-3xl font-semibold tracking-tight">{t("welcome.title")}</h1>
|
||||
|
||||
Reference in new issue
Block a user