mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
feat: portable play settings, improved offline messaging, and system tray support
This commit is contained in:
1 parent
925a81f4f2
commit
88747c8da1
13 files changed
+154
-8
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
area: manager
|
||||||
|
type: added
|
||||||
|
audience: admins
|
||||||
|
title: Control portable characters and keep Manager running in the system tray
|
||||||
|
---
|
||||||
|
Realm access settings can enable portable characters. Closing the window keeps Manager in the tray, with commands to reopen or exit. Offline realm messages explain why players cannot connect.
|
||||||
@@ -445,6 +445,17 @@
|
|||||||
"max": 3600000,
|
"max": 3600000,
|
||||||
"unit": "ms"
|
"unit": "ms"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "PortableSession.Enable",
|
||||||
|
"type": "bool",
|
||||||
|
"category": "characters",
|
||||||
|
"title": "Portable characters",
|
||||||
|
"description": "Allow players to play on this realm with portable characters saved in their Manager.",
|
||||||
|
"default": true,
|
||||||
|
"advanced": false,
|
||||||
|
"restartRequired": "world",
|
||||||
|
"dangerous": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "CharactersPerAccount",
|
"key": "CharactersPerAccount",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = ["tray-icon"] }
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
|
|||||||
+70
-3
@@ -2729,6 +2729,8 @@ async fn console_command(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static QUIT_REQUESTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let dir = data_dir();
|
let dir = data_dir();
|
||||||
let _ = coa_core::logging::init(&dir.join("logs").join("manager.log"));
|
let _ = coa_core::logging::init(&dir.join("logs").join("manager.log"));
|
||||||
@@ -2881,11 +2883,75 @@ pub fn run() {
|
|||||||
control_cmds::join_characters,
|
control_cmds::join_characters,
|
||||||
control_cmds::join_claim
|
control_cmds::join_claim
|
||||||
])
|
])
|
||||||
|
.on_window_event(|window, event| {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
api.prevent_close();
|
||||||
|
let _ = window.hide();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setup(|app| {
|
||||||
|
#[cfg(desktop)]
|
||||||
|
{
|
||||||
|
use tauri::menu::{Menu, MenuItem};
|
||||||
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
let show = MenuItem::with_id(app, "show", "Open CoA Server Manager", true, None::<&str>)?;
|
||||||
|
let quit = MenuItem::with_id(app, "quit", "Exit", true, None::<&str>)?;
|
||||||
|
let menu = Menu::with_items(app, &[&show, &quit])?;
|
||||||
|
|
||||||
|
let mut builder = TrayIconBuilder::new()
|
||||||
|
.tooltip("CoA Server Manager")
|
||||||
|
.menu(&menu)
|
||||||
|
.show_menu_on_left_click(false)
|
||||||
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
|
"show" => {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"quit" => {
|
||||||
|
QUIT_REQUESTED.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if let TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
let app = tray.app_handle();
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(icon) = app.default_window_icon() {
|
||||||
|
builder = builder.icon(icon.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build(app)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building CoA Server Manager")
|
.expect("error while building CoA Server Manager")
|
||||||
.run(|app, event| {
|
.run(|app, event| match event {
|
||||||
// the Host loop stops with the application: what it is doing is finished first, the stores keep every session
|
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||||
if let tauri::RunEvent::Exit = event {
|
if !QUIT_REQUESTED.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tauri::RunEvent::Exit => {
|
||||||
|
// the Host loop stops with the application: what it is doing is finished first, the stores keep every session
|
||||||
if let Some(rt) = app.state::<AppState>().portable.clone() {
|
if let Some(rt) = app.state::<AppState>().portable.clone() {
|
||||||
rt.shutdown();
|
rt.shutdown();
|
||||||
}
|
}
|
||||||
@@ -2896,6 +2962,7 @@ pub fn run() {
|
|||||||
rt.shutdown();
|
rt.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1006,7 +1006,7 @@ const en = {
|
|||||||
"portable.note.blocked_ruleset": "This character is for another kind of realm.",
|
"portable.note.blocked_ruleset": "This character is for another kind of realm.",
|
||||||
"portable.note.blocked_format": "This realm reads another version of the character format. Update the Manager or the realm.",
|
"portable.note.blocked_format": "This realm reads another version of the character format. Update the Manager or the realm.",
|
||||||
"portable.note.blocked_online_import": "This realm cannot take characters while it runs. Update the realm.",
|
"portable.note.blocked_online_import": "This realm cannot take characters while it runs. Update the realm.",
|
||||||
"portable.note.blocked_sessions": "This realm cannot follow a game session. Ask its owner to enable portable play.",
|
"portable.note.blocked_sessions": "This realm cannot follow a game session. The server or host's Server Manager is offline, or portable play is not enabled.",
|
||||||
"portable.note.blocked_client_data": "The game data the Manager sees is not the data the realm loaded.",
|
"portable.note.blocked_client_data": "The game data the Manager sees is not the data the realm loaded.",
|
||||||
"portable.note.blocked_progression": "The realm’s level cap is not known yet or cannot be handled. Start the realm and try again.",
|
"portable.note.blocked_progression": "The realm’s level cap is not known yet or cannot be handled. Start the realm and try again.",
|
||||||
"portable.note.realm_offline": "The realm cannot be reached.",
|
"portable.note.realm_offline": "The realm cannot be reached.",
|
||||||
@@ -1101,6 +1101,8 @@ const en = {
|
|||||||
"browse.noCharacter": "You have no portable character yet. Make one portable on the Characters page to see whether it can play here.",
|
"browse.noCharacter": "You have no portable character yet. Make one portable on the Characters page to see whether it can play here.",
|
||||||
"browse.canPlay": "Can play",
|
"browse.canPlay": "Can play",
|
||||||
"browse.cannotPlay": "Cannot play here",
|
"browse.cannotPlay": "Cannot play here",
|
||||||
|
"browse.realmOffline": "Offline",
|
||||||
|
"browse.offlineWarning": "The server is currently offline. Joining with a portable character will become available once the host starts the server.",
|
||||||
"browse.play": "Play",
|
"browse.play": "Play",
|
||||||
"browse.playSoon": "Joining arrives with the account support of the next step.",
|
"browse.playSoon": "Joining arrives with the account support of the next step.",
|
||||||
"registry.title": "Public listing",
|
"registry.title": "Public listing",
|
||||||
@@ -1121,6 +1123,7 @@ const en = {
|
|||||||
"registry.state.starting": "Starting to publish…",
|
"registry.state.starting": "Starting to publish…",
|
||||||
"registry.state.waiting_for_realm": "Waiting for the realm to be ready…",
|
"registry.state.waiting_for_realm": "Waiting for the realm to be ready…",
|
||||||
"registry.state.realm_stopped": "The realm is not running; it is listed as offline.",
|
"registry.state.realm_stopped": "The realm is not running; it is listed as offline.",
|
||||||
|
"registry.realmStoppedHint": "The realm is currently stopped. Start the server so players can join and track character sessions.",
|
||||||
"registry.state.online": "Published and online.",
|
"registry.state.online": "Published and online.",
|
||||||
"registry.state.retrying": "The Registry could not be reached; trying again. Your realm is not affected.",
|
"registry.state.retrying": "The Registry could not be reached; trying again. Your realm is not affected.",
|
||||||
"registry.state.rejected": "The Registry refused this realm. Publishing is stopped.",
|
"registry.state.rejected": "The Registry refused this realm. Publishing is stopped.",
|
||||||
@@ -1180,6 +1183,8 @@ const en = {
|
|||||||
"join.err.unavailable": "The server could not do that right now.",
|
"join.err.unavailable": "The server could not do that right now.",
|
||||||
"access.title": "Joining players",
|
"access.title": "Joining players",
|
||||||
"access.text": "How players who find this realm in the list get in.",
|
"access.text": "How players who find this realm in the list get in.",
|
||||||
|
"access.portableEnabled": "Allow portable characters (portable play)",
|
||||||
|
"access.portableEnabledHint": "Allows players with Manager-saved characters to play on this realm (PortableSession.Enable).",
|
||||||
"access.existingOnly": "Do not create accounts for new players",
|
"access.existingOnly": "Do not create accounts for new players",
|
||||||
"access.existingOnlyHint": "Players can only link an account they already have on this realm.",
|
"access.existingOnlyHint": "Players can only link an account they already have on this realm.",
|
||||||
"access.route": "Address players connect to",
|
"access.route": "Address players connect to",
|
||||||
|
|||||||
@@ -988,7 +988,7 @@ const ru: Partial<Record<keyof typeof en, string>> = {
|
|||||||
"portable.note.blocked_ruleset": "Этот персонаж — для реалма другого типа.",
|
"portable.note.blocked_ruleset": "Этот персонаж — для реалма другого типа.",
|
||||||
"portable.note.blocked_format": "Реалм читает другую версию формата персонажа. Обновите Менеджер или реалм.",
|
"portable.note.blocked_format": "Реалм читает другую версию формата персонажа. Обновите Менеджер или реалм.",
|
||||||
"portable.note.blocked_online_import": "Этот реалм не принимает персонажей, пока работает. Обновите реалм.",
|
"portable.note.blocked_online_import": "Этот реалм не принимает персонажей, пока работает. Обновите реалм.",
|
||||||
"portable.note.blocked_sessions": "Этот реалм не может следить за игровой сессией. Попросите владельца включить переносимую игру.",
|
"portable.note.blocked_sessions": "Этот реалм не может следить за игровой сессией. Сервер или Server Manager хоста выключен, либо в ядре не включена переносимая игра.",
|
||||||
"portable.note.blocked_client_data": "Игровые данные, которые видит Менеджер, не те, что загрузил реалм.",
|
"portable.note.blocked_client_data": "Игровые данные, которые видит Менеджер, не те, что загрузил реалм.",
|
||||||
"portable.note.blocked_progression": "Предел уровня реалма пока неизвестен или не поддерживается. Запустите реалм и попробуйте снова.",
|
"portable.note.blocked_progression": "Предел уровня реалма пока неизвестен или не поддерживается. Запустите реалм и попробуйте снова.",
|
||||||
"portable.note.realm_offline": "Реалм недоступен.",
|
"portable.note.realm_offline": "Реалм недоступен.",
|
||||||
@@ -1083,6 +1083,8 @@ const ru: Partial<Record<keyof typeof en, string>> = {
|
|||||||
"browse.noCharacter": "У вас пока нет переносимого персонажа. Сделайте его переносимым на странице «Персонажи», чтобы увидеть, сможет ли он играть здесь.",
|
"browse.noCharacter": "У вас пока нет переносимого персонажа. Сделайте его переносимым на странице «Персонажи», чтобы увидеть, сможет ли он играть здесь.",
|
||||||
"browse.canPlay": "Можно играть",
|
"browse.canPlay": "Можно играть",
|
||||||
"browse.cannotPlay": "Здесь играть нельзя",
|
"browse.cannotPlay": "Здесь играть нельзя",
|
||||||
|
"browse.realmOffline": "Оффлайн",
|
||||||
|
"browse.offlineWarning": "Сервер сейчас не запущен хостом. Игра переносимым персонажем станет доступна, когда владелец включит сервер.",
|
||||||
"browse.play": "Играть",
|
"browse.play": "Играть",
|
||||||
"browse.playSoon": "Вход на сервер появится вместе с поддержкой аккаунтов на следующем шаге.",
|
"browse.playSoon": "Вход на сервер появится вместе с поддержкой аккаунтов на следующем шаге.",
|
||||||
"registry.title": "Публичный список",
|
"registry.title": "Публичный список",
|
||||||
@@ -1103,6 +1105,7 @@ const ru: Partial<Record<keyof typeof en, string>> = {
|
|||||||
"registry.state.starting": "Начинаю публикацию…",
|
"registry.state.starting": "Начинаю публикацию…",
|
||||||
"registry.state.waiting_for_realm": "Жду, пока реалм будет готов…",
|
"registry.state.waiting_for_realm": "Жду, пока реалм будет готов…",
|
||||||
"registry.state.realm_stopped": "Реалм не запущен; в списке он отмечен как недоступный.",
|
"registry.state.realm_stopped": "Реалм не запущен; в списке он отмечен как недоступный.",
|
||||||
|
"registry.realmStoppedHint": "Сервер сейчас выключен. Запустите реалм, чтобы игроки могли подключаться и отслеживать сессии персонажей.",
|
||||||
"registry.state.online": "Опубликован и доступен.",
|
"registry.state.online": "Опубликован и доступен.",
|
||||||
"registry.state.retrying": "Реестр недоступен, пробую снова. На ваш реалм это не влияет.",
|
"registry.state.retrying": "Реестр недоступен, пробую снова. На ваш реалм это не влияет.",
|
||||||
"registry.state.rejected": "Реестр отклонил этот реалм. Публикация остановлена.",
|
"registry.state.rejected": "Реестр отклонил этот реалм. Публикация остановлена.",
|
||||||
@@ -1162,6 +1165,8 @@ const ru: Partial<Record<keyof typeof en, string>> = {
|
|||||||
"join.err.unavailable": "Сервер сейчас не смог этого сделать.",
|
"join.err.unavailable": "Сервер сейчас не смог этого сделать.",
|
||||||
"access.title": "Вход игроков",
|
"access.title": "Вход игроков",
|
||||||
"access.text": "Как попадают на этот мир игроки, нашедшие его в списке.",
|
"access.text": "Как попадают на этот мир игроки, нашедшие его в списке.",
|
||||||
|
"access.portableEnabled": "Разрешить вход переносимым персонажам (переносимая игра)",
|
||||||
|
"access.portableEnabledHint": "Позволяет игрокам с сохранёнными в Менеджере персонажами играть на этом сервере (PortableSession.Enable).",
|
||||||
"access.existingOnly": "Не создавать аккаунты для новых игроков",
|
"access.existingOnly": "Не создавать аккаунты для новых игроков",
|
||||||
"access.existingOnlyHint": "Игроки смогут только привязать аккаунт, который у них уже есть на этом мире.",
|
"access.existingOnlyHint": "Игроки смогут только привязать аккаунт, который у них уже есть на этом мире.",
|
||||||
"access.route": "Адрес для подключения игроков",
|
"access.route": "Адрес для подключения игроков",
|
||||||
|
|||||||
@@ -380,6 +380,10 @@
|
|||||||
"t": "Autospeicher-Intervall",
|
"t": "Autospeicher-Intervall",
|
||||||
"d": "Wie oft Spielercharaktere gespeichert werden, solange sie online sind."
|
"d": "Wie oft Spielercharaktere gespeichert werden, solange sie online sind."
|
||||||
},
|
},
|
||||||
|
"PortableSession.Enable": {
|
||||||
|
"t": "Portable Charaktere (portables Spiel)",
|
||||||
|
"d": "Erlaubt Spielern, mit ihren im Manager gespeicherten portablen Charakteren auf diesem Realm zu spielen."
|
||||||
|
},
|
||||||
"CharactersPerAccount": {
|
"CharactersPerAccount": {
|
||||||
"t": "Charaktere pro Konto",
|
"t": "Charaktere pro Konto",
|
||||||
"d": "Maximale Zahl an Charakteren, die ein Konto über alle Realms hat."
|
"d": "Maximale Zahl an Charakteren, die ein Konto über alle Realms hat."
|
||||||
|
|||||||
@@ -380,6 +380,10 @@
|
|||||||
"t": "Intervalo de autoguardado",
|
"t": "Intervalo de autoguardado",
|
||||||
"d": "Con qué frecuencia se guardan los personajes de los jugadores mientras están conectados."
|
"d": "Con qué frecuencia se guardan los personajes de los jugadores mientras están conectados."
|
||||||
},
|
},
|
||||||
|
"PortableSession.Enable": {
|
||||||
|
"t": "Personajes portátiles (juego portátil)",
|
||||||
|
"d": "Permite a los jugadores jugar en este reino con personajes portátiles guardados en su Manager."
|
||||||
|
},
|
||||||
"CharactersPerAccount": {
|
"CharactersPerAccount": {
|
||||||
"t": "Personajes por cuenta",
|
"t": "Personajes por cuenta",
|
||||||
"d": "Máximo de personajes que puede tener una cuenta entre todos los reinos."
|
"d": "Máximo de personajes que puede tener una cuenta entre todos los reinos."
|
||||||
|
|||||||
@@ -380,6 +380,10 @@
|
|||||||
"t": "Intervalle de sauvegarde automatique",
|
"t": "Intervalle de sauvegarde automatique",
|
||||||
"d": "Fréquence de sauvegarde des personnages des joueurs pendant qu’ils sont en ligne."
|
"d": "Fréquence de sauvegarde des personnages des joueurs pendant qu’ils sont en ligne."
|
||||||
},
|
},
|
||||||
|
"PortableSession.Enable": {
|
||||||
|
"t": "Personnages portables (jeu portable)",
|
||||||
|
"d": "Permet aux joueurs de jouer sur ce royaume avec des personnages portables enregistrés dans leur Manager."
|
||||||
|
},
|
||||||
"CharactersPerAccount": {
|
"CharactersPerAccount": {
|
||||||
"t": "Personnages par compte",
|
"t": "Personnages par compte",
|
||||||
"d": "Nombre maximal de personnages qu’un compte peut avoir sur tous les royaumes."
|
"d": "Nombre maximal de personnages qu’un compte peut avoir sur tous les royaumes."
|
||||||
|
|||||||
@@ -380,6 +380,10 @@
|
|||||||
"t": "Интервал автосохранения",
|
"t": "Интервал автосохранения",
|
||||||
"d": "Как часто персонажи игроков сохраняются, пока они в сети."
|
"d": "Как часто персонажи игроков сохраняются, пока они в сети."
|
||||||
},
|
},
|
||||||
|
"PortableSession.Enable": {
|
||||||
|
"t": "Переносимые персонажи (переносимая игра)",
|
||||||
|
"d": "Разрешает игрокам заходить на этот сервер переносимыми персонажами, сохранёнными в их Менеджере."
|
||||||
|
},
|
||||||
"CharactersPerAccount": {
|
"CharactersPerAccount": {
|
||||||
"t": "Персонажей на учётную запись",
|
"t": "Персонажей на учётную запись",
|
||||||
"d": "Наибольшее число персонажей на учётной записи во всех мирах."
|
"d": "Наибольшее число персонажей на учётной записи во всех мирах."
|
||||||
|
|||||||
@@ -380,6 +380,10 @@
|
|||||||
"t": "角色数据自动保存间隔",
|
"t": "角色数据自动保存间隔",
|
||||||
"d": "在线玩家角色数据写入数据库的时间间隔。"
|
"d": "在线玩家角色数据写入数据库的时间间隔。"
|
||||||
},
|
},
|
||||||
|
"PortableSession.Enable": {
|
||||||
|
"t": "便携式角色(跨服漫游)",
|
||||||
|
"d": "允许玩家使用保存在管理器中的便携式角色进入此服务器进行游玩。"
|
||||||
|
},
|
||||||
"CharactersPerAccount": {
|
"CharactersPerAccount": {
|
||||||
"t": "每个账号最大角色数",
|
"t": "每个账号最大角色数",
|
||||||
"d": "单个账号在所有服务器可创建的角色总数上限。"
|
"d": "单个账号在所有服务器可创建的角色总数上限。"
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog:
|
|||||||
<h2 className="truncate text-lg font-semibold" data-testid="drawer-title">{summary.display_name}</h2>
|
<h2 className="truncate text-lg font-semibold" data-testid="drawer-title">{summary.display_name}</h2>
|
||||||
<p className="mt-0.5 text-sm text-muted"><Mode ruleset={summary.ruleset} /> · {t("browse.cap")} {summary.level_cap ?? "—"}</p>
|
<p className="mt-0.5 text-sm text-muted"><Mode ruleset={summary.ruleset} /> · {t("browse.cap")} {summary.level_cap ?? "—"}</p>
|
||||||
<p className="text-sm"><Online p={summary.population} />{summary.population.capacity !== null && <span className="text-muted"> · {t("browse.capacity", { n: summary.population.capacity })}</span>}</p>
|
<p className="text-sm"><Online p={summary.population} />{summary.population.capacity !== null && <span className="text-muted"> · {t("browse.capacity", { n: summary.population.capacity })}</span>}</p>
|
||||||
|
{!summary.online && <span className="mt-1 inline-block rounded bg-bad/15 px-2 py-0.5 text-xs font-medium text-bad">{t("browse.realmOffline")}</span>}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} aria-label={t("portable.close")} className="cursor-pointer text-muted hover:text-ink"><X className="h-5 w-5" /></button>
|
<button onClick={onClose} aria-label={t("portable.close")} className="cursor-pointer text-muted hover:text-ink"><X className="h-5 w-5" /></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,6 +181,7 @@ function Drawer({ summary, catalog, onClose }: { summary: RealmSummary; catalog:
|
|||||||
<p className="font-medium">{blocked ? t("browse.cannotPlay") : t("browse.canPlay")}</p>
|
<p className="font-medium">{blocked ? t("browse.cannotPlay") : t("browse.canPlay")}</p>
|
||||||
{pre.projection && <Projection from={pre.projection[0]} to={pre.projection[1]} cap={summary.level_cap} />}
|
{pre.projection && <Projection from={pre.projection[0]} to={pre.projection[1]} cap={summary.level_cap} />}
|
||||||
<Notes notes={pre.notes} />
|
<Notes notes={pre.notes} />
|
||||||
|
{!summary.online && <p className="mt-2 border-t border-line/40 pt-2 text-xs text-warn">{t("browse.offlineWarning")}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
</>}
|
</>}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useT, type Key } from "@/i18n";
|
import { useT, type Key } from "@/i18n";
|
||||||
import { asUiError } from "@/lib/api";
|
import { asUiError, api } from "@/lib/api";
|
||||||
import { realmRegistry, useRegistryStatus } from "@/lib/registry";
|
import { realmRegistry, useRegistryStatus } from "@/lib/registry";
|
||||||
import { asControlError, control, type ControlStatus } from "@/lib/control";
|
import { asControlError, control, type ControlStatus } from "@/lib/control";
|
||||||
|
|
||||||
@@ -25,12 +25,25 @@ export function PublishCard({ serverId, serverName }: { serverId: string; server
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [allowPortable, setAllowPortable] = useState(false);
|
const [allowPortable, setAllowPortable] = useState(false);
|
||||||
const [existingOnly, setExistingOnly] = useState(false);
|
const [existingOnly, setExistingOnly] = useState(false);
|
||||||
|
const [portableEnabled, setPortableEnabled] = useState(true);
|
||||||
const [route, setRoute] = useState("");
|
const [route, setRoute] = useState("");
|
||||||
const [accessSaved, setAccessSaved] = useState(false);
|
const [accessSaved, setAccessSaved] = useState(false);
|
||||||
const [ctl, setCtl] = useState<ControlStatus | null>(null);
|
const [ctl, setCtl] = useState<ControlStatus | null>(null);
|
||||||
const host = ctl?.hosting.find((h) => h.local_id === localId);
|
const host = ctl?.hosting.find((h) => h.local_id === localId);
|
||||||
const link = host?.link;
|
const link = host?.link;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
void api.allSettings(serverId).then((items) => {
|
||||||
|
if (!active) return;
|
||||||
|
const item = items.find((i) => i.key === "PortableSession.Enable");
|
||||||
|
if (item) {
|
||||||
|
setPortableEnabled(item.value === "1" || item.value.toLowerCase() === "true");
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
return () => { active = false; };
|
||||||
|
}, [serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = () => void control.status().then(setCtl).catch(() => {});
|
const load = () => void control.status().then(setCtl).catch(() => {});
|
||||||
load();
|
load();
|
||||||
@@ -41,7 +54,21 @@ export function PublishCard({ serverId, serverName }: { serverId: string; server
|
|||||||
|
|
||||||
async function saveAccess() {
|
async function saveAccess() {
|
||||||
setBusy(true); setError(null); setAccessSaved(false);
|
setBusy(true); setError(null); setAccessSaved(false);
|
||||||
try { await control.setAccess(localId, existingOnly, allowPortable, route.trim() === "" ? null : route.trim()); await refresh(); setAccessSaved(true); } catch (e) { setError(asControlError(e).message); } finally { setBusy(false); }
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
control.setAccess(localId, existingOnly, allowPortable, route.trim() === "" ? null : route.trim()),
|
||||||
|
api.allSettingsSave(serverId, {
|
||||||
|
"PortableSession.Enable": portableEnabled ? "1" : "0",
|
||||||
|
...(portableEnabled ? { "CharacterDatabase.WorkerThreads": "1" } : {}),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
await refresh();
|
||||||
|
setAccessSaved(true);
|
||||||
|
} catch (e) {
|
||||||
|
setError(asControlError(e).message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -85,6 +112,7 @@ export function PublishCard({ serverId, serverName }: { serverId: string; server
|
|||||||
{mine && (published || mine.state === "unpublishing") && <div className="mt-4 rounded-md border border-line p-3 text-sm" role="status">
|
{mine && (published || mine.state === "unpublishing") && <div className="mt-4 rounded-md border border-line p-3 text-sm" role="status">
|
||||||
<p className={cn("font-medium", tone)} data-state={mine.state}>{t(`registry.state.${mine.state}` as Key)}</p>
|
<p className={cn("font-medium", tone)} data-state={mine.state}>{t(`registry.state.${mine.state}` as Key)}</p>
|
||||||
<p className="mt-1 text-xs text-muted">{mine.display_name}{mine.metadata_revision ? ` · ${t("registry.revision", { n: mine.metadata_revision })}` : ""}</p>
|
<p className="mt-1 text-xs text-muted">{mine.display_name}{mine.metadata_revision ? ` · ${t("registry.revision", { n: mine.metadata_revision })}` : ""}</p>
|
||||||
|
{mine.state === "realm_stopped" && <p className="mt-2 text-xs text-warn">{t("registry.realmStoppedHint")}</p>}
|
||||||
{mine.last_error && <p className="mt-1 text-xs text-warn">{mine.last_error}</p>}
|
{mine.last_error && <p className="mt-1 text-xs text-warn">{mine.last_error}</p>}
|
||||||
{mine.state === "retrying" && mine.retry_in_secs !== null && <p className="mt-1 text-xs text-muted">{t("registry.retryIn", { s: mine.retry_in_secs })}</p>}
|
{mine.state === "retrying" && mine.retry_in_secs !== null && <p className="mt-1 text-xs text-muted">{t("registry.retryIn", { s: mine.retry_in_secs })}</p>}
|
||||||
<div className="mt-3 flex gap-2">
|
<div className="mt-3 flex gap-2">
|
||||||
@@ -95,6 +123,7 @@ export function PublishCard({ serverId, serverName }: { serverId: string; server
|
|||||||
{mine && (published || mine.state === "unpublishing") && <div className="mt-4 rounded-md border border-line p-3 text-sm" data-testid="access-options">
|
{mine && (published || mine.state === "unpublishing") && <div className="mt-4 rounded-md border border-line p-3 text-sm" data-testid="access-options">
|
||||||
<p className="font-medium">{t("access.title")}</p>
|
<p className="font-medium">{t("access.title")}</p>
|
||||||
<p className="mt-1 text-xs text-muted">{t("access.text")}</p>
|
<p className="mt-1 text-xs text-muted">{t("access.text")}</p>
|
||||||
|
<label className="mt-3 flex items-start gap-2"><input type="checkbox" checked={portableEnabled} onChange={(e) => setPortableEnabled(e.target.checked)} className="mt-1" data-testid="portable-enabled" /><span>{t("access.portableEnabled")}<span className="block text-xs text-muted">{t("access.portableEnabledHint")}</span></span></label>
|
||||||
<label className="mt-3 flex items-start gap-2"><input type="checkbox" checked={existingOnly} onChange={(e) => setExistingOnly(e.target.checked)} className="mt-1" data-testid="existing-only" /><span>{t("access.existingOnly")}<span className="block text-xs text-muted">{t("access.existingOnlyHint")}</span></span></label>
|
<label className="mt-3 flex items-start gap-2"><input type="checkbox" checked={existingOnly} onChange={(e) => setExistingOnly(e.target.checked)} className="mt-1" data-testid="existing-only" /><span>{t("access.existingOnly")}<span className="block text-xs text-muted">{t("access.existingOnlyHint")}</span></span></label>
|
||||||
<label className="mt-3 flex items-start gap-2"><input type="checkbox" checked={allowPortable} onChange={(e) => setAllowPortable(e.target.checked)} className="mt-1" data-testid="allow-portable" /><span>{t("access.allowPortable")}<span className="block text-xs text-muted">{t("access.allowPortableHint")}</span></span></label>
|
<label className="mt-3 flex items-start gap-2"><input type="checkbox" checked={allowPortable} onChange={(e) => setAllowPortable(e.target.checked)} className="mt-1" data-testid="allow-portable" /><span>{t("access.allowPortable")}<span className="block text-xs text-muted">{t("access.allowPortableHint")}</span></span></label>
|
||||||
<label htmlFor="access-route" className="mt-3 block font-medium">{t("access.route")}</label>
|
<label htmlFor="access-route" className="mt-3 block font-medium">{t("access.route")}</label>
|
||||||
|
|||||||
Reference in new issue
Block a user