gnss/frontend/src/components/SettingsPanel.tsx

294 lines
10 KiB
TypeScript
Raw Normal View History

import type { MutableRefObject, RefObject } from "react";
import { FormEvent } from "react";
import type { AppConfig, MapMode, SliceParams } from "../types";
type SettingsPanelProps = {
open: boolean;
onClose: () => void;
config: AppConfig | null;
status: string;
theme: "dark" | "light";
onToggleTheme: () => void;
mapMode: MapMode;
onMapModeChange: (mode: MapMode) => void;
tilesets: string[];
activeTileset: string;
onActivateTileset: (name: string) => void;
onRefreshTilesets: () => void;
onCreateTileset: (name: string) => void;
sliceParams: SliceParams;
onSliceParamsChange: (params: SliceParams) => void;
sliceStatus: string;
onSliceSubmit: (event?: FormEvent<HTMLFormElement>) => void;
onSliceReset: () => void;
selectedFileName: string;
onTriggerFileSelect: () => void;
onFileSelected: (file: File | null | undefined) => void;
fileInputRef: RefObject<HTMLInputElement | null> | MutableRefObject<HTMLInputElement | null>;
};
export const SettingsPanel = ({
open,
onClose,
config,
status,
theme,
onToggleTheme,
mapMode,
onMapModeChange,
tilesets,
activeTileset,
onActivateTileset,
onRefreshTilesets,
onCreateTileset,
sliceParams,
onSliceParamsChange,
sliceStatus,
onSliceSubmit,
onSliceReset,
selectedFileName,
onTriggerFileSelect,
onFileSelected,
fileInputRef,
}: SettingsPanelProps) => (
<>
<div className={`settings-backdrop ${open ? "open" : ""}`} onClick={onClose} />
<aside className={`settings-panel ${open ? "open" : ""}`}>
<div className="settings-header">
<div>
<div className="panel-title">Настройки</div>
<div className="panel-subtitle">Управление источниками и нарезкой</div>
</div>
<button type="button" className="btn primary small" onClick={onClose}>
Закрыть
</button>
</div>
<div className="settings-content">
<section className="panel info">
<div className="info-grid">
<div className="card">
<div className="card-label">GNSS источник</div>
<div className="card-value">{config?.gnss_source ?? "—"}</div>
</div>
<div className="card">
<div className="card-label">Набор тайлов</div>
<div className="card-value">{activeTileset || "—"}</div>
</div>
<div className="card">
<div className="card-label">Статус</div>
<div className="card-value">{status}</div>
</div>
</div>
</section>
<section className="panel info">
<div className="panel-header">
<div>
<div className="panel-title">Тема</div>
<div className="panel-subtitle">Светлая или тёмная</div>
</div>
<button type="button" className="btn ghost small" onClick={onToggleTheme}>
{theme === "dark" ? "Включить светлую" : "Включить тёмную"}
</button>
</div>
<label className="field compact">
<span>Режим карты</span>
<select value={mapMode} onChange={(e) => onMapModeChange(e.target.value as MapMode)}>
<option value="raster">Растровые</option>
<option value="vector">Векторные</option>
</select>
</label>
</section>
<section className="panel">
<div className="panel-header">
<div>
<div className="panel-title">Наборы тайлов</div>
<div className="panel-subtitle">Активация и обновление</div>
</div>
</div>
<div className="tileset-row stacked">
<select value={activeTileset} onChange={(e) => onActivateTileset(e.target.value)}>
{tilesets.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<button type="button" className="btn ghost" onClick={onRefreshTilesets}>
Обновить
</button>
</div>
</section>
<section className="panel slice-panel">
<div className="panel-header">
<div>
<div className="panel-title">Нарезка тайлов</div>
<div className="panel-subtitle">Запускает gdal2tiles на бэкенде</div>
</div>
<div className={`badge ${sliceStatus === "ok" ? "ok" : sliceStatus === "idle" ? "" : "warn"}`}>
{sliceStatus}
</div>
</div>
<form
className="slice-grid"
onSubmit={(event) => {
event.preventDefault();
onSliceSubmit(event);
}}
>
<label className="field">
<span>Источник (GeoTIFF/MBTiles)</span>
<div className="file-row">
<input
type="text"
placeholder="/path/to/input.tif"
value={sliceParams.source}
onChange={(e) => onSliceParamsChange({ ...sliceParams, source: e.target.value })}
required
/>
<button type="button" className="btn ghost" onClick={onTriggerFileSelect}>
Выбрать
</button>
<input
type="file"
accept=".tif,.tiff,.mbtiles,.jpg,.jpeg,.png"
ref={fileInputRef}
style={{ display: "none" }}
onChange={(e) => onFileSelected(e.target.files?.[0])}
/>
</div>
{selectedFileName && <small className="muted">Файл: {selectedFileName}</small>}
</label>
<label className="field">
<span>Схема тайлов</span>
<select
value={sliceParams.scheme}
onChange={(e) => onSliceParamsChange({ ...sliceParams, scheme: e.target.value as "xyz" | "tms" })}
>
<option value="xyz">XYZ (Y сверху, Leaflet)</option>
<option value="tms">TMS (Y снизу)</option>
</select>
</label>
<label className="field">
<span>Формат исходника</span>
<select
value={sliceParams.source_format}
onChange={(e) =>
onSliceParamsChange({
...sliceParams,
source_format: e.target.value as "geotiff" | "mbtiles" | "tms" | "xyz",
})
}
>
<option value="geotiff">GeoTIFF/растровый TIFF</option>
<option value="mbtiles">MBTiles (растровый)</option>
<option value="tms">Папка TMS</option>
<option value="xyz">Папка XYZ</option>
</select>
</label>
<label className="field">
<span>Tileset (имя папки)</span>
<div className="file-row">
<input
type="text"
list="tilesets-list"
placeholder="например: moscow (пусто → активный)"
value={sliceParams.tileset}
onChange={(e) => onSliceParamsChange({ ...sliceParams, tileset: e.target.value })}
/>
<button
type="button"
className="btn ghost"
onClick={() => {
const name = sliceParams.tileset.trim();
if (!name) return;
onCreateTileset(name);
}}
>
Создать
</button>
</div>
<datalist id="tilesets-list">
{tilesets.map((t) => (
<option key={t} value={t} />
))}
</datalist>
<small className="muted">Активный: {activeTileset || "—"}</small>
</label>
<label className="field">
<span>Папка тайлов (опц.)</span>
<input
type="text"
placeholder="лучше не трогать (по умолчанию tileset)"
value={sliceParams.destination}
onChange={(e) => onSliceParamsChange({ ...sliceParams, destination: e.target.value })}
/>
</label>
<label className="field">
<span>Min zoom</span>
<input
type="number"
min={0}
value={sliceParams.min_zoom}
onChange={(e) =>
onSliceParamsChange({
...sliceParams,
min_zoom: Number(e.target.value),
})
}
required
/>
</label>
<label className="field">
<span>Max zoom</span>
<input
type="number"
min={0}
value={sliceParams.max_zoom}
onChange={(e) =>
onSliceParamsChange({
...sliceParams,
max_zoom: Number(e.target.value),
})
}
required
/>
</label>
<label className="field">
<span>Формат тайлов (сейчас поддерживается PNG)</span>
<select
value={sliceParams.tile_format}
onChange={(e) => onSliceParamsChange({ ...sliceParams, tile_format: e.target.value })}
>
<option value="png">png</option>
</select>
</label>
<div className="field actions">
<span>Действия</span>
<div className="actions-row">
<button type="submit" className="btn primary">
Запустить
</button>
<button type="button" className="btn ghost" onClick={onSliceReset}>
Сброс
</button>
</div>
</div>
</form>
</section>
</div>
</aside>
</>
);