From fe7a98098fb9d91edc2fb8f14efa017849f94da1 Mon Sep 17 00:00:00 2001 From: DanSylvest Date: Mon, 7 Jul 2025 16:57:06 +0300 Subject: [PATCH] fix(Map): Unified settings. Second part: Import/Export --- .../js/hooks/Mapper/common-styles/fixes.scss | 72 ++++++ .../common-styles/prime-fixes/fix-dialog.scss | 6 +- .../hooks/Mapper/components/helpers/index.ts | 1 + .../helpers/parseMapUserSettings.ts | 67 ++++++ .../SystemSignatures/SystemSignatures.tsx | 2 +- .../mapRootContent/MapRootContent.tsx | 5 +- .../components/MapSettings/MapSettings.tsx | 5 + .../MapSettings/components/ImportExport.tsx | 205 +++++++++++++++++ .../components/OldSettingsDialog.tsx | 206 ++++++++++++++++++ .../js/hooks/Mapper/constants/signatures.ts | 1 - .../mapRootProvider/MapRootProvider.tsx | 9 + .../mapRootProvider/hooks/api/useMapInit.ts | 4 - .../hooks/useMapUserSettings.ts | 62 +++++- .../mapRootProvider/hooks/useStoreWidgets.ts | 36 +-- assets/js/hooks/Mapper/utils/index.ts | 2 + assets/js/hooks/Mapper/utils/loadTextFile.ts | 27 +++ assets/js/hooks/Mapper/utils/saveToFile.ts | 36 +++ 17 files changed, 697 insertions(+), 49 deletions(-) create mode 100644 assets/js/hooks/Mapper/components/helpers/index.ts create mode 100644 assets/js/hooks/Mapper/components/helpers/parseMapUserSettings.ts create mode 100644 assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx create mode 100644 assets/js/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx create mode 100644 assets/js/hooks/Mapper/utils/loadTextFile.ts create mode 100644 assets/js/hooks/Mapper/utils/saveToFile.ts diff --git a/assets/js/hooks/Mapper/common-styles/fixes.scss b/assets/js/hooks/Mapper/common-styles/fixes.scss index ccd456ed..8673024d 100644 --- a/assets/js/hooks/Mapper/common-styles/fixes.scss +++ b/assets/js/hooks/Mapper/common-styles/fixes.scss @@ -212,3 +212,75 @@ .p-inputtext:enabled:hover { 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; +} + diff --git a/assets/js/hooks/Mapper/common-styles/prime-fixes/fix-dialog.scss b/assets/js/hooks/Mapper/common-styles/prime-fixes/fix-dialog.scss index 43a97739..ee0a9da3 100644 --- a/assets/js/hooks/Mapper/common-styles/prime-fixes/fix-dialog.scss +++ b/assets/js/hooks/Mapper/common-styles/prime-fixes/fix-dialog.scss @@ -64,9 +64,9 @@ body .p-dialog { } .p-dialog-footer { - padding: 1rem; - border-top: 1px solid #ddd; - background: #f4f4f4; + padding: .75rem 1rem; + border-top: none !important; + //background: #f4f4f4; } .p-dialog-header-close { diff --git a/assets/js/hooks/Mapper/components/helpers/index.ts b/assets/js/hooks/Mapper/components/helpers/index.ts new file mode 100644 index 00000000..65a24000 --- /dev/null +++ b/assets/js/hooks/Mapper/components/helpers/index.ts @@ -0,0 +1 @@ +export * from './parseMapUserSettings.ts'; diff --git a/assets/js/hooks/Mapper/components/helpers/parseMapUserSettings.ts b/assets/js/hooks/Mapper/components/helpers/parseMapUserSettings.ts new file mode 100644 index 00000000..532d1b9c --- /dev/null +++ b/assets/js/hooks/Mapper/components/helpers/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 => + 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 => + 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`); + } + } + + // Everything passes, so cast is safe + return data as MapUserSettings; +}; + +/* ------------------------------ Usage example ----------------------------- */ + +// const raw = fetchFromServer(); // string +// const settings = parseMapUserSettings(raw); diff --git a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx index 6bba86a9..5656cfe3 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatures.tsx @@ -8,7 +8,7 @@ import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey'; import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts'; import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers'; 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. diff --git a/assets/js/hooks/Mapper/components/mapRootContent/MapRootContent.tsx b/assets/js/hooks/Mapper/components/mapRootContent/MapRootContent.tsx index d6541cf9..f4844d0e 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/MapRootContent.tsx +++ b/assets/js/hooks/Mapper/components/mapRootContent/MapRootContent.tsx @@ -14,13 +14,14 @@ import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/compone import { useMapEventListener } from '@/hooks/Mapper/events'; import { Commands } from '@/hooks/Mapper/types'; import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components'; +import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx'; export interface MapRootContentProps {} // eslint-disable-next-line no-empty-pattern export const MapRootContent = ({}: MapRootContentProps) => { const { - storedSettings: { interfaceSettings, isReady }, + storedSettings: { interfaceSettings, isReady, hasOldSettings }, data, } = useMapRootState(); const { isShowMenu } = interfaceSettings; @@ -90,6 +91,8 @@ export const MapRootContent = ({}: MapRootContentProps) => { {showTrackingDialog && ( setShowTrackingDialog(false)} /> )} + + {hasOldSettings && } ); diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx index 26b8c113..7989d5b2 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx @@ -12,6 +12,7 @@ import { import { WidgetsSettings } from './components/WidgetsSettings'; import { CommonSettings } from './components/CommonSettings'; import { SettingsListItem } from './types.ts'; +import { ImportExport } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx'; export interface MapSettingsProps { visible: boolean; @@ -87,6 +88,10 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => { + + + + diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx new file mode 100644 index 00000000..97365821 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx @@ -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(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 ( +
+
+
+ +
+ + + *Will read map settings from clipboard. Be careful it could overwrite current settings. + +
+ +
+
+ +
+ + *Will save map settings to clipboard. +
+ + +
+ ); +}; diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx new file mode 100644 index 00000000..fd61df48 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx @@ -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 (lsSettings: string | null, defaultValues: T) { + return { + version: -1, + settings: lsSettings ? JSON.parse(lsSettings) : defaultValues, + }; +}; + +export const OldSettingsDialog = () => { + const cpRemoveBtnRef = useRef(); + const [cpRemoveVisible, setCpRemoveVisible] = useState(false); + const handleShowCP = useCallback(() => setCpRemoveVisible(true), []); + const handleHideCP = useCallback(() => setCpRemoveVisible(false), []); + const toast = useRef(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 ( + <> + + Old settings detected! + + } + draggable={false} + resizable={false} + closable={false} + visible + onHide={() => null} + className="w-[640px] h-[400px] text-text-color min-h-0" + footer={ +
+
+ } + > +
+ + We detected deprecated settings saved in your browser. + + + Now we will give you ability to make export your old settings. + + + After click: all settings will saved in your clipboard. + + + Then you need to go into Map Settings and click{' '} + Import from clipboard + +
+ +
+
+ + *You will see this dialog until click Export. +
+
+ + + + + + ); +}; diff --git a/assets/js/hooks/Mapper/constants/signatures.ts b/assets/js/hooks/Mapper/constants/signatures.ts index da499489..1b62c915 100644 --- a/assets/js/hooks/Mapper/constants/signatures.ts +++ b/assets/js/hooks/Mapper/constants/signatures.ts @@ -1,6 +1,5 @@ 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 enum SIGNATURES_DELETION_TIMING { diff --git a/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx b/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx index 20b8cd5f..ba2aa53a 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx +++ b/assets/js/hooks/Mapper/mapRootProvider/MapRootProvider.tsx @@ -23,6 +23,7 @@ import { InterfaceStoredSettings, KillsWidgetSettings, LocalWidgetSettings, + MapUserSettings, OnTheMapSettingsType, RoutesType, } from '@/hooks/Mapper/mapRootProvider/types.ts'; @@ -127,6 +128,10 @@ export interface MapRootContextProps { settingsKills: KillsWidgetSettings; settingsKillsUpdate: Dispatch>; isReady: boolean; + hasOldSettings: boolean; + getSettingsForExport(): string | undefined; + applySettings(settings: MapUserSettings): boolean; + checkOldSettings(): void; }; } @@ -167,6 +172,10 @@ const MapRootContext = createContext({ settingsKills: DEFAULT_KILLS_WIDGET_SETTINGS, settingsKillsUpdate: () => null, isReady: false, + hasOldSettings: false, + getSettingsForExport: () => '', + applySettings: () => false, + checkOldSettings: () => null, }, }); diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts index 6ade4ad8..622dbb12 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/api/useMapInit.ts @@ -28,12 +28,8 @@ export const useMapInit = () => { following_character_eve_id, user_hubs, map_slug, - ...rest } = props; - // eslint-disable-next-line no-console - console.log('JOipP', `rest`, rest); - const updateData: Partial = {}; if (wormholes) { diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/useMapUserSettings.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/useMapUserSettings.ts index a1b12b81..6e4a2ef7 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/hooks/useMapUserSettings.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/useMapUserSettings.ts @@ -8,10 +8,12 @@ import { getDefaultWidgetProps, STORED_INTERFACE_DEFAULT_VALUES, } 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 { MapRootData } from '@/hooks/Mapper/mapRootProvider'; import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/useSettingsValueAndSetter.ts'; +import fastDeepEqual from 'fast-deep-equal'; + // import { actualizeSettings } from '@/hooks/Mapper/mapRootProvider/helpers'; // TODO - we need provide and compare version @@ -37,12 +39,15 @@ const createDefaultWidgetSettings = (): MapUserSettings => { const EMPTY_OBJ = {}; export const useMapUserSettings = ({ map_slug }: MapRootData) => { + const [isReady, setIsReady] = useState(false); + const [hasOldSettings, setHasOldSettings] = useState(false); + const [mapUserSettings, setMapUserSettings] = useLocalStorageState('map-user-settings', { defaultValue: EMPTY_OBJ, }); - const ref = useRef({ mapUserSettings, setMapUserSettings }); - ref.current = { mapUserSettings, setMapUserSettings }; + const ref = useRef({ mapUserSettings, setMapUserSettings, map_slug }); + ref.current = { mapUserSettings, setMapUserSettings, map_slug }; useEffect(() => { const { mapUserSettings, setMapUserSettings } = ref.current; @@ -107,8 +112,6 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => { 'widgets', ); - const [isReady, setIsReady] = useState(false); - // HERE we MUST work with migrations useEffect(() => { if (isReady) { @@ -150,8 +153,53 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => { 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 { isReady, + hasOldSettings, + interfaceSettings, setInterfaceSettings, settingsRoutes, @@ -166,5 +214,9 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => { settingsKillsUpdate, windowsSettings, setWindowsSettings, + + getSettingsForExport, + applySettings, + checkOldSettings, }; }; diff --git a/assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts b/assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts index 41fcf831..03252d0f 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts @@ -1,11 +1,6 @@ -import { - CURRENT_WINDOWS_VERSION, - DEFAULT_WIDGETS, - WidgetsIds, - WINDOWS_LOCAL_STORE_KEY, -} from '@/hooks/Mapper/components/mapInterface/constants.tsx'; +import { DEFAULT_WIDGETS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx'; 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 { 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()), []); return { diff --git a/assets/js/hooks/Mapper/utils/index.ts b/assets/js/hooks/Mapper/utils/index.ts index 5a9c58bd..0c18203d 100644 --- a/assets/js/hooks/Mapper/utils/index.ts +++ b/assets/js/hooks/Mapper/utils/index.ts @@ -1,2 +1,4 @@ export * from './contextStore'; export * from './getQueryVariable'; +export * from './loadTextFile'; +export * from './saveToFile'; diff --git a/assets/js/hooks/Mapper/utils/loadTextFile.ts b/assets/js/hooks/Mapper/utils/loadTextFile.ts new file mode 100644 index 00000000..004dc0fe --- /dev/null +++ b/assets/js/hooks/Mapper/utils/loadTextFile.ts @@ -0,0 +1,27 @@ +export function loadTextFile(): Promise { + 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(); + }); +} diff --git a/assets/js/hooks/Mapper/utils/saveToFile.ts b/assets/js/hooks/Mapper/utils/saveToFile.ts new file mode 100644 index 00000000..17086514 --- /dev/null +++ b/assets/js/hooks/Mapper/utils/saveToFile.ts @@ -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(); +}