feat: local browser saves with opt-in cloud sync behind saves.cloud permission
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4418da47eb
commit
47fc121e7d
8 files changed
+377
-31
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useAuth } from "../auth";
|
||||
import { cloudEnabled, exportSaves, importSaves, setCloudEnabled } from "../player/saves";
|
||||
import { PageHeader } from "../ui/Bits";
|
||||
import { Icon } from "../ui/Icon";
|
||||
import { useUi } from "../ui/Ui";
|
||||
|
||||
export function Saves() {
|
||||
const { can } = useAuth();
|
||||
const ui = useUi();
|
||||
const allowed = can("saves.cloud");
|
||||
const [cloud, setCloud] = useState(cloudEnabled());
|
||||
const file = useRef<HTMLInputElement>(null);
|
||||
|
||||
const toggle = (on: boolean) => {
|
||||
setCloudEnabled(on);
|
||||
setCloud(on);
|
||||
ui.toast(on ? "Облачные сохранения включены на этом устройстве" : "Облачные сохранения выключены", "ok");
|
||||
};
|
||||
|
||||
const download = async () => {
|
||||
const url = URL.createObjectURL(new Blob([await exportSaves()], { type: "application/json" }));
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "pgames-saves.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const upload = async (f: File | undefined) => {
|
||||
if (!f) return;
|
||||
try {
|
||||
const n = await importSaves(await f.text());
|
||||
ui.toast(n ? `Загружено сохранений: ${n}` : "Новых сохранений в файле нет", "ok");
|
||||
} catch (e) {
|
||||
ui.toast((e as Error).message, "error");
|
||||
}
|
||||
if (file.current) file.current.value = "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Сохранения" subtitle="По умолчанию хранятся только в этом браузере" />
|
||||
<section className="card">
|
||||
<h2>Облако</h2>
|
||||
<p className="muted">
|
||||
Облачные сохранения копируются на сервер и доступны с других устройств.
|
||||
Включаются отдельно на каждом устройстве; если сервер недоступен, игра сохраняется локально.
|
||||
</p>
|
||||
{allowed ? (
|
||||
<label className="row">
|
||||
<input type="checkbox" checked={cloud} onChange={(e) => toggle(e.target.checked)} />
|
||||
Синхронизировать сохранения с сервером
|
||||
</label>
|
||||
) : (
|
||||
<p className="muted">У вашей роли нет права «Облачные сохранения» — обратитесь к администратору.</p>
|
||||
)}
|
||||
</section>
|
||||
<section className="card">
|
||||
<h2>Перенос файлом</h2>
|
||||
<p className="muted">Скачайте все локальные сохранения или загрузите файл с другого устройства.</p>
|
||||
<div className="actions">
|
||||
<button className="btn" onClick={() => void download()}><Icon name="download" size={18} />Скачать</button>
|
||||
<button className="btn" onClick={() => file.current?.click()}><Icon name="upload" size={18} />Загрузить</button>
|
||||
<input ref={file} type="file" accept="application/json,.json" hidden
|
||||
onChange={(e) => void upload(e.target.files?.[0])} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user