fix(Map): Unified settings. Second part: Import/Export

This commit is contained in:
DanSylvest
2025-07-07 16:57:06 +03:00
parent df49939990
commit fe7a98098f
17 changed files with 697 additions and 49 deletions

View File

@@ -212,3 +212,75 @@
.p-inputtext:enabled:hover { .p-inputtext:enabled:hover {
border-color: #335c7e; border-color: #335c7e;
} }
// --------------- TOAST
.p-toast .p-toast-message {
background-color: #1a1a1a;
color: #e0e0e0;
border-left: 4px solid transparent;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
}
.p-toast .p-toast-message .p-toast-summary {
color: #ffffff;
font-weight: 600;
}
.p-toast .p-toast-message .p-toast-detail {
color: #c0c0c0;
font-size: 13px;
}
.p-toast .p-toast-icon-close {
color: #ffaa00;
transition: background 0.2s;
}
.p-toast .p-toast-icon-close:hover {
background: #333;
color: #fff;
}
.p-toast-message-success {
border-left-color: #f1c40f;
}
.p-toast-message-error {
border-left-color: #e74c3c;
}
.p-toast-message-info {
border-left-color: #3498db;
}
.p-toast-message-warn {
border-left-color: #e67e22;
}
.p-toast-message-success .p-toast-message-icon {
color: #f1c40f;
}
.p-toast-message-error .p-toast-message-icon {
color: #e74c3c;
}
.p-toast-message-info .p-toast-message-icon {
color: #3498db;
}
.p-toast-message-warn .p-toast-message-icon {
color: #e67e22;
}
.p-toast-message-success .p-toast-message-content {
border-left-color: #f1c40f;
}
.p-toast-message-error .p-toast-message-content {
border-left-color: #e74c3c;
}
.p-toast-message-info .p-toast-message-content {
border-left-color: #3498db;
}
.p-toast-message-warn .p-toast-message-content {
border-left-color: #e67e22;
}

View File

@@ -64,9 +64,9 @@ body .p-dialog {
} }
.p-dialog-footer { .p-dialog-footer {
padding: 1rem; padding: .75rem 1rem;
border-top: 1px solid #ddd; border-top: none !important;
background: #f4f4f4; //background: #f4f4f4;
} }
.p-dialog-header-close { .p-dialog-header-close {

View File

@@ -0,0 +1 @@
export * from './parseMapUserSettings.ts';

View File

@@ -0,0 +1,67 @@
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
const REQUIRED_KEYS = [
'widgets',
'interface',
'onTheMap',
'routes',
'localWidget',
'signaturesWidget',
'killsWidget',
] as const;
type RequiredKeys = (typeof REQUIRED_KEYS)[number];
/** Custom error for any parsing / validation issue */
export class MapUserSettingsParseError extends Error {
constructor(msg: string) {
super(`MapUserSettings parse error: ${msg}`);
}
}
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
typeof v === 'object' && v !== null && REQUIRED_KEYS.every(k => k in v);
/* ------------------------------ Main parser ------------------------------- */
/**
* Parses and validates a JSON string as `MapUserSettings`.
*
* @throws `MapUserSettingsParseError` если строка не JSON или нарушена структура
*/
export const parseMapUserSettings = (json: unknown): MapUserSettings => {
if (typeof json !== 'string') throw new MapUserSettingsParseError('Input must be a JSON string');
let data: unknown;
try {
data = JSON.parse(json);
} catch (e) {
throw new MapUserSettingsParseError(`Invalid JSON: ${(e as Error).message}`);
}
if (!hasAllRequiredKeys(data)) {
const missing = REQUIRED_KEYS.filter(k => !(k in (data as any)));
throw new MapUserSettingsParseError(`Missing top-level field(s): ${missing.join(', ')}`);
}
for (const key of REQUIRED_KEYS) {
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
}
}
// Everything passes, so cast is safe
return data as MapUserSettings;
};
/* ------------------------------ Usage example ----------------------------- */
// const raw = fetchFromServer(); // string
// const settings = parseMapUserSettings(raw);

View File

@@ -8,7 +8,7 @@ import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers'; import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types'; import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts'; import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
/** /**
* Custom hook for managing pending signature deletions and undo countdown. * Custom hook for managing pending signature deletions and undo countdown.

View File

@@ -14,13 +14,14 @@ import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/compone
import { useMapEventListener } from '@/hooks/Mapper/events'; import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types'; import { Commands } from '@/hooks/Mapper/types';
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components'; import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx';
export interface MapRootContentProps {} export interface MapRootContentProps {}
// eslint-disable-next-line no-empty-pattern // eslint-disable-next-line no-empty-pattern
export const MapRootContent = ({}: MapRootContentProps) => { export const MapRootContent = ({}: MapRootContentProps) => {
const { const {
storedSettings: { interfaceSettings, isReady }, storedSettings: { interfaceSettings, isReady, hasOldSettings },
data, data,
} = useMapRootState(); } = useMapRootState();
const { isShowMenu } = interfaceSettings; const { isShowMenu } = interfaceSettings;
@@ -90,6 +91,8 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showTrackingDialog && ( {showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} /> <TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)} )}
{hasOldSettings && <OldSettingsDialog />}
</Layout> </Layout>
</div> </div>
); );

View File

@@ -12,6 +12,7 @@ import {
import { WidgetsSettings } from './components/WidgetsSettings'; import { WidgetsSettings } from './components/WidgetsSettings';
import { CommonSettings } from './components/CommonSettings'; import { CommonSettings } from './components/CommonSettings';
import { SettingsListItem } from './types.ts'; import { SettingsListItem } from './types.ts';
import { ImportExport } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx';
export interface MapSettingsProps { export interface MapSettingsProps {
visible: boolean; visible: boolean;
@@ -87,6 +88,10 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}> <TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings /> <WidgetsSettings />
</TabPanel> </TabPanel>
<TabPanel header="Import/Export" className="h-full" headerClassName={styles.verticalTabHeader}>
<ImportExport />
</TabPanel>
</TabView> </TabView>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,205 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useMemo, useRef } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
import { SplitButton } from 'primereact/splitbutton';
import { loadTextFile } from '@/hooks/Mapper/utils';
export const ImportExport = () => {
const {
storedSettings: { getSettingsForExport, applySettings },
data: { map_slug },
} = useMapRootState();
const toast = useRef<Toast | null>(null);
const handleImportFromClipboard = useCallback(async () => {
const text = await navigator.clipboard.readText();
if (text == null || text == '') {
return;
}
try {
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
detail: 'Map settings was imported successfully.',
life: 3000,
});
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 100);
return;
}
toast.current?.show({
severity: 'warn',
summary: 'Warning',
detail: 'Settings already imported. Or something went wrong.',
life: 3000,
});
} catch (error) {
console.error(`Import from clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on import from Clipboard, check console log.',
life: 3000,
});
}
}, [applySettings]);
const handleImportFromFile = useCallback(async () => {
try {
const text = await loadTextFile();
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
toast.current?.show({
severity: 'success',
summary: 'Import',
detail: 'Map settings was imported successfully.',
life: 3000,
});
return;
}
toast.current?.show({
severity: 'warn',
summary: 'Warning',
detail: 'Settings already imported. Or something went wrong.',
life: 3000,
});
// eslint-disable-next-line no-console
console.log('JOipP', `text`, text);
} catch (error) {
console.error(`Import from file Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on import from File, check console log.',
life: 3000,
});
}
}, [applySettings]);
const handleExportToClipboard = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
return;
}
try {
await navigator.clipboard.writeText(settings);
toast.current?.show({
severity: 'success',
summary: 'Export',
detail: 'Map settings copied into clipboard',
life: 3000,
});
} catch (error) {
console.error(`Export to clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on copying to clipboard, check console log.',
life: 3000,
});
}
}, [getSettingsForExport]);
const handleExportToFile = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
return;
}
try {
saveTextFile(`map_settings_${map_slug}.json`, settings);
toast.current?.show({
severity: 'success',
summary: 'Export to File',
detail: 'Map settings successfully saved to file',
life: 3000,
});
} catch (error) {
console.error(`Export to cliboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on saving to file, check console log.',
life: 3000,
});
}
}, [getSettingsForExport, map_slug]);
const importItems = useMemo(
() => [
{
label: 'Import from File',
icon: 'pi pi-file-import',
command: handleImportFromFile,
},
],
[handleImportFromFile],
);
const exportItems = useMemo(
() => [
{
label: 'Export as File',
icon: 'pi pi-file-export',
command: handleExportToFile,
},
],
[handleExportToFile],
);
return (
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<SplitButton
onClick={handleImportFromClipboard}
icon="pi pi-download"
size="small"
severity="warning"
label="Import from Clipboard"
className="py-[4px]"
model={importItems}
/>
</div>
<span className="text-stone-500 text-[12px]">
*Will read map settings from clipboard. Be careful it could overwrite current settings.
</span>
</div>
<div className="flex flex-col gap-1">
<div>
<SplitButton
onClick={handleExportToClipboard}
icon="pi pi-upload"
size="small"
label="Export to Clipboard"
className="py-[4px]"
model={exportItems}
/>
</div>
<span className="text-stone-500 text-[12px]">*Will save map settings to clipboard.</span>
</div>
<Toast ref={toast} />
</div>
);
};

View File

@@ -0,0 +1,206 @@
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useCallback, useRef, useState } from 'react';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { Toast } from 'primereact/toast';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { saveTextFile } from '@/hooks/Mapper/utils';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return {
version: -1,
settings: lsSettings ? JSON.parse(lsSettings) : defaultValues,
};
};
export const OldSettingsDialog = () => {
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const toast = useRef<Toast | null>(null);
const {
storedSettings: { checkOldSettings },
data: { map_slug },
} = useMapRootState();
const handleExport = useCallback(
async (asFile?: boolean) => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
const widgetRoutes = localStorage.getItem('window:interface:routes');
const widgetLocal = localStorage.getItem('window:interface:local');
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_5');
const out: MapUserSettings = {
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createSettings(widgetLocal, DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createSettings(widgetsOld, getDefaultWidgetProps()),
routes: createSettings(widgetRoutes, DEFAULT_ROUTES_SETTINGS),
onTheMap: createSettings(onTheMapOld, DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createSettings(signatures, DEFAULT_SIGNATURE_SETTINGS),
interface: createSettings(interfaceSettings, STORED_INTERFACE_DEFAULT_VALUES),
};
if (asFile) {
if (!out) {
return;
}
try {
saveTextFile(`map_settings_${map_slug}.json`, JSON.stringify(out));
toast.current?.show({
severity: 'success',
summary: 'Export to File',
detail: 'Map settings successfully saved to file',
life: 3000,
});
} catch (error) {
console.error(`Export to cliboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on saving to file, check console log.',
life: 3000,
});
return;
}
return;
}
try {
await navigator.clipboard.writeText(JSON.stringify(out));
toast.current?.show({
severity: 'success',
summary: 'Export to clipboard',
detail: 'Map settings was export successfully.',
life: 3000,
});
} catch (error) {
console.error(`Export to clipboard Error: `, error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Some error occurred on copying to clipboard, check console log.',
life: 3000,
});
}
},
[map_slug],
);
const handleExportClipboard = useCallback(async () => {
await handleExport();
}, [handleExport]);
const handleExportAsFile = useCallback(async () => {
await handleExport(true);
}, [handleExport]);
const handleProceed = useCallback(() => {
localStorage.removeItem('window:interface:settings');
localStorage.removeItem('window:interface:routes');
localStorage.removeItem('window:interface:local');
localStorage.removeItem('kills:widget:settings');
localStorage.removeItem('window:onTheMap:settings');
localStorage.removeItem('windows:settings:v2');
localStorage.removeItem('wanderer_system_signature_settings_v6_5');
checkOldSettings();
}, [checkOldSettings]);
return (
<>
<Dialog
header={
<div className="dialog-header">
<span className="pointer-events-none">Old settings detected!</span>
</div>
}
draggable={false}
resizable={false}
closable={false}
visible
onHide={() => null}
className="w-[640px] h-[400px] text-text-color min-h-0"
footer={
<div className="flex items-center justify-end">
<Button
// @ts-ignore
ref={cpRemoveBtnRef}
onClick={handleShowCP}
icon="pi pi-exclamation-triangle"
size="small"
severity="warning"
label="Proceed"
/>
</div>
}
>
<div className="w-full h-full flex flex-col gap-1 items-center justify-center text-stone-400 text-[15px]">
<span>
We detected <span className="text-orange-400">deprecated</span> settings saved in your browser.
</span>
<span>
Now we will give you ability to make <span className="text-orange-400">export</span> your old settings.
</span>
<span>
After click: all settings will saved in your <span className="text-orange-400">clipboard</span>.
</span>
<span>
Then you need to go into <span className="text-orange-400">Map Settings</span> and click{' '}
<span className="text-orange-400">Import from clipboard</span>
</span>
<div className="h-[30px]"></div>
<div className="flex items-center gap-3">
<Button
onClick={handleExportClipboard}
icon="pi pi-copy"
size="small"
severity="info"
label="Export to Clipboard"
/>
<Button
onClick={handleExportAsFile}
icon="pi pi-download"
size="small"
severity="info"
label="Export as File"
/>
</div>
<span className="text-stone-600 text-[12px]">*You will see this dialog until click Export.</span>
</div>
</Dialog>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="After click dialog will disappear. Ready?"
icon="pi pi-exclamation-triangle"
accept={handleProceed}
/>
<Toast ref={toast} />
</>
);
};

View File

@@ -1,6 +1,5 @@
import { SignatureGroup, SignatureKind } from '@/hooks/Mapper/types'; import { SignatureGroup, SignatureKind } from '@/hooks/Mapper/types';
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
export const SIGNATURE_WINDOW_ID = 'system_signatures_window'; export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export enum SIGNATURES_DELETION_TIMING { export enum SIGNATURES_DELETION_TIMING {

View File

@@ -23,6 +23,7 @@ import {
InterfaceStoredSettings, InterfaceStoredSettings,
KillsWidgetSettings, KillsWidgetSettings,
LocalWidgetSettings, LocalWidgetSettings,
MapUserSettings,
OnTheMapSettingsType, OnTheMapSettingsType,
RoutesType, RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts'; } from '@/hooks/Mapper/mapRootProvider/types.ts';
@@ -127,6 +128,10 @@ export interface MapRootContextProps {
settingsKills: KillsWidgetSettings; settingsKills: KillsWidgetSettings;
settingsKillsUpdate: Dispatch<SetStateAction<KillsWidgetSettings>>; settingsKillsUpdate: Dispatch<SetStateAction<KillsWidgetSettings>>;
isReady: boolean; isReady: boolean;
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
checkOldSettings(): void;
}; };
} }
@@ -167,6 +172,10 @@ const MapRootContext = createContext<MapRootContextProps>({
settingsKills: DEFAULT_KILLS_WIDGET_SETTINGS, settingsKills: DEFAULT_KILLS_WIDGET_SETTINGS,
settingsKillsUpdate: () => null, settingsKillsUpdate: () => null,
isReady: false, isReady: false,
hasOldSettings: false,
getSettingsForExport: () => '',
applySettings: () => false,
checkOldSettings: () => null,
}, },
}); });

View File

@@ -28,12 +28,8 @@ export const useMapInit = () => {
following_character_eve_id, following_character_eve_id,
user_hubs, user_hubs,
map_slug, map_slug,
...rest
} = props; } = props;
// eslint-disable-next-line no-console
console.log('JOipP', `rest`, rest);
const updateData: Partial<MapRootData> = {}; const updateData: Partial<MapRootData> = {};
if (wormholes) { if (wormholes) {

View File

@@ -8,10 +8,12 @@ import {
getDefaultWidgetProps, getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES, STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts'; } from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures'; import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures';
import { MapRootData } from '@/hooks/Mapper/mapRootProvider'; import { MapRootData } from '@/hooks/Mapper/mapRootProvider';
import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/useSettingsValueAndSetter.ts'; import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/useSettingsValueAndSetter.ts';
import fastDeepEqual from 'fast-deep-equal';
// import { actualizeSettings } from '@/hooks/Mapper/mapRootProvider/helpers'; // import { actualizeSettings } from '@/hooks/Mapper/mapRootProvider/helpers';
// TODO - we need provide and compare version // TODO - we need provide and compare version
@@ -37,12 +39,15 @@ const createDefaultWidgetSettings = (): MapUserSettings => {
const EMPTY_OBJ = {}; const EMPTY_OBJ = {};
export const useMapUserSettings = ({ map_slug }: MapRootData) => { export const useMapUserSettings = ({ map_slug }: MapRootData) => {
const [isReady, setIsReady] = useState(false);
const [hasOldSettings, setHasOldSettings] = useState(false);
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>('map-user-settings', { const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>('map-user-settings', {
defaultValue: EMPTY_OBJ, defaultValue: EMPTY_OBJ,
}); });
const ref = useRef({ mapUserSettings, setMapUserSettings }); const ref = useRef({ mapUserSettings, setMapUserSettings, map_slug });
ref.current = { mapUserSettings, setMapUserSettings }; ref.current = { mapUserSettings, setMapUserSettings, map_slug };
useEffect(() => { useEffect(() => {
const { mapUserSettings, setMapUserSettings } = ref.current; const { mapUserSettings, setMapUserSettings } = ref.current;
@@ -107,8 +112,6 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => {
'widgets', 'widgets',
); );
const [isReady, setIsReady] = useState(false);
// HERE we MUST work with migrations // HERE we MUST work with migrations
useEffect(() => { useEffect(() => {
if (isReady) { if (isReady) {
@@ -150,8 +153,53 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => {
isReady, isReady,
]); ]);
const checkOldSettings = useCallback(() => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
const widgetRoutes = localStorage.getItem('window:interface:routes');
const widgetLocal = localStorage.getItem('window:interface:local');
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
setHasOldSettings(!!(widgetsOld || interfaceSettings || widgetRoutes || widgetLocal || widgetKills || onTheMapOld));
}, []);
useEffect(() => {
checkOldSettings();
}, [checkOldSettings]);
const getSettingsForExport = useCallback(() => {
const { map_slug } = ref.current;
if (map_slug == null) {
return;
}
return JSON.stringify(ref.current.mapUserSettings[map_slug]);
}, []);
const applySettings = useCallback((settings: MapUserSettings) => {
const { map_slug, mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug == null) {
return false;
}
if (fastDeepEqual(settings, mapUserSettings[map_slug])) {
return false;
}
setMapUserSettings(old => ({
...old,
[map_slug]: settings,
}));
return true;
}, []);
return { return {
isReady, isReady,
hasOldSettings,
interfaceSettings, interfaceSettings,
setInterfaceSettings, setInterfaceSettings,
settingsRoutes, settingsRoutes,
@@ -166,5 +214,9 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => {
settingsKillsUpdate, settingsKillsUpdate,
windowsSettings, windowsSettings,
setWindowsSettings, setWindowsSettings,
getSettingsForExport,
applySettings,
checkOldSettings,
}; };
}; };

View File

@@ -1,11 +1,6 @@
import { import { DEFAULT_WIDGETS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
WidgetsIds,
WINDOWS_LOCAL_STORE_KEY,
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts'; import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react'; import { Dispatch, SetStateAction, useCallback, useRef } from 'react';
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager'; import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
import { getDefaultWidgetProps } from '@/hooks/Mapper/mapRootProvider/constants.ts'; import { getDefaultWidgetProps } from '@/hooks/Mapper/mapRootProvider/constants.ts';
@@ -77,33 +72,6 @@ export const useStoreWidgets = ({ windowsSettings, setWindowsSettings }: UseStor
}); });
}, []); }, []);
useEffect(() => {
const { setWindowsSettings } = ref.current;
const raw = localStorage.getItem(WINDOWS_LOCAL_STORE_KEY);
if (!raw) {
console.warn('No windows found in local storage!!');
setWindowsSettings(getDefaultWidgetProps());
return;
}
const { version, windows, visible, viewPort } = JSON.parse(raw) as WindowStoreInfo;
if (!version || CURRENT_WINDOWS_VERSION > version) {
setWindowsSettings(getDefaultWidgetProps());
}
// eslint-disable-next-line no-debugger
const out = windows.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id));
setWindowsSettings({
version: CURRENT_WINDOWS_VERSION,
windows: out as WindowProps[],
visible,
viewPort,
});
}, []);
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []); const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
return { return {

View File

@@ -1,2 +1,4 @@
export * from './contextStore'; export * from './contextStore';
export * from './getQueryVariable'; export * from './getQueryVariable';
export * from './loadTextFile';
export * from './saveToFile';

View File

@@ -0,0 +1,27 @@
export function loadTextFile(): Promise<string> {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsText(file);
};
input.click();
});
}

View File

@@ -0,0 +1,36 @@
export function saveTextFile(filename: string, content: string) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
// эмулируем клик
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// освобождаем URL
URL.revokeObjectURL(url);
}
export async function saveTextFileInteractive(filename: string, content: string) {
if (!('showSaveFilePicker' in window)) {
throw new Error('File System Access API is not supported in this browser.');
}
const handle = await (window as any).showSaveFilePicker({
suggestedName: filename,
types: [
{
description: 'Text Files',
accept: { 'text/plain': ['.txt', '.json'] },
},
],
});
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}