mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
fix(Map): Unified settings. Second part: Import/Export
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
1
assets/js/hooks/Mapper/components/helpers/index.ts
Normal file
1
assets/js/hooks/Mapper/components/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './parseMapUserSettings.ts';
|
||||||
@@ -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);
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './contextStore';
|
export * from './contextStore';
|
||||||
export * from './getQueryVariable';
|
export * from './getQueryVariable';
|
||||||
|
export * from './loadTextFile';
|
||||||
|
export * from './saveToFile';
|
||||||
|
|||||||
27
assets/js/hooks/Mapper/utils/loadTextFile.ts
Normal file
27
assets/js/hooks/Mapper/utils/loadTextFile.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
36
assets/js/hooks/Mapper/utils/saveToFile.ts
Normal file
36
assets/js/hooks/Mapper/utils/saveToFile.ts
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user