diff --git a/assets/js/hooks/Mapper/components/map/helpers/getSystemClassGroup.ts b/assets/js/hooks/Mapper/components/map/helpers/getSystemClassGroup.ts new file mode 100644 index 00000000..6052a38c --- /dev/null +++ b/assets/js/hooks/Mapper/components/map/helpers/getSystemClassGroup.ts @@ -0,0 +1,18 @@ +import { + SOLAR_SYSTEM_CLASS_IDS, + SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS, +} from '@/hooks/Mapper/components/map/constants.ts'; + +export const getSystemClassGroup = (systemClassId: number | undefined | null): string | null => { + if (systemClassId == null) return null; + + const systemClassKey = Object.keys(SOLAR_SYSTEM_CLASS_IDS).find( + key => SOLAR_SYSTEM_CLASS_IDS[key as keyof typeof SOLAR_SYSTEM_CLASS_IDS] === systemClassId, + ); + + if (!systemClassKey) return null; + + return ( + SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS[systemClassKey as keyof typeof SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS] || null + ); +}; diff --git a/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/SystemLinkSignatureDialog.tsx b/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/SystemLinkSignatureDialog.tsx index 38883825..be95bc12 100644 --- a/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/SystemLinkSignatureDialog.tsx +++ b/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/SystemLinkSignatureDialog.tsx @@ -1,20 +1,21 @@ import { Dialog } from 'primereact/dialog'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useSystemInfo } from '@/hooks/Mapper/components/hooks'; import { - SOLAR_SYSTEM_CLASS_IDS, - SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS, - WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME, + SOLAR_SYSTEM_CLASS_IDS, + SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS, + WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME, } from '@/hooks/Mapper/components/map/constants.ts'; import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent'; import { MULTI_DEST_WHS, ALL_DEST_TYPES_MAP, DEST_TYPES_MAP_MAP } from '@/hooks/Mapper/constants.ts'; import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures'; +import { getSystemClassGroup } from '@/hooks/Mapper/components/map/helpers/getSystemClassGroup.ts'; import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types'; -import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; import { useSystemSignaturesData } from '../../widgets/SystemSignatures/hooks/useSystemSignaturesData'; +import { useLinkSignature } from './hooks/useLinkSignature'; const MULTI_DEST_TYPES = MULTI_DEST_WHS.map((type: string) => WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME[type].shortName); @@ -23,7 +24,7 @@ interface SystemLinkSignatureDialogProps { setVisible: (visible: boolean) => void; } -export const LINK_SIGNTATURE_SETTINGS: SignatureSettingsType = { +export const LINK_SIGNATURE_SETTINGS: SignatureSettingsType = { [SETTINGS_KEYS.COSMIC_SIGNATURE]: true, [SETTINGS_KEYS.WORMHOLE]: true, [SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true, @@ -38,13 +39,9 @@ interface ExtendedSignatureCustomInfo { export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignatureDialogProps) => { const { - outCommand, data: { wormholes }, } = useMapRootState(); - const ref = useRef({ outCommand }); - ref.current = { outCommand }; - // Get system info for the target system const { staticInfo: targetSystemInfo, dynamicInfo: targetSystemDynamicInfo } = useSystemInfo({ systemId: `${data.solar_system_target}`, @@ -53,17 +50,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat // Get the system class group for the target system const targetSystemClassGroup = useMemo(() => { if (!targetSystemInfo) return null; - const systemClassId = targetSystemInfo.system_class; - - const systemClassKey = Object.keys(SOLAR_SYSTEM_CLASS_IDS).find( - key => SOLAR_SYSTEM_CLASS_IDS[key as keyof typeof SOLAR_SYSTEM_CLASS_IDS] === systemClassId, - ); - - if (!systemClassKey) return null; - - return ( - SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS[systemClassKey as keyof typeof SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS] || null - ); + return getSystemClassGroup(targetSystemInfo.system_class); }, [targetSystemInfo]); const handleHide = useCallback(() => { @@ -115,32 +102,26 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat [targetSystemClassGroup, wormholes], ); + const { signatures } = useSystemSignaturesData({ + systemId: `${data.solar_system_source}`, + settings: LINK_SIGNATURE_SETTINGS, + }); + + const { handleLinkSignature } = useLinkSignature({ data, targetSystemClassGroup }); + const handleSelect = useCallback( - (signature: SystemSignature) => { + async (signature: SystemSignature) => { if (!signature) { return; } - const { outCommand } = ref.current; - - outCommand({ - type: OutCommand.linkSignatureToSystem, - data: { - ...data, - signature_eve_id: signature.eve_id, - }, - }); + await handleLinkSignature(signature); setVisible(false); }, - [data, setVisible], + [handleLinkSignature, setVisible], ); - const { signatures } = useSystemSignaturesData({ - systemId: `${data.solar_system_source}`, - settings: LINK_SIGNTATURE_SETTINGS, - }); - useEffect(() => { if (!targetSystemDynamicInfo) { handleHide(); @@ -160,7 +141,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat systemId={`${data.solar_system_source}`} signatures={signatures} hasUnsupportedLanguage={false} - settings={LINK_SIGNTATURE_SETTINGS} + settings={LINK_SIGNATURE_SETTINGS} hideLinkedSignatures selectable onSelect={handleSelect} diff --git a/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/hooks/useLinkSignature.ts b/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/hooks/useLinkSignature.ts new file mode 100644 index 00000000..bdb9a211 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapInterface/components/SystemLinkSignatureDialog/hooks/useLinkSignature.ts @@ -0,0 +1,155 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { handleAutoBookmark, numberToLetters } from '@/hooks/Mapper/helpers/bookmarkFormatHelper.ts'; +import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo'; +import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; +import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types'; +import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; +import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts'; + +export interface UseLinkSignatureProps { + data: CommandLinkSignatureToSystem; + targetSystemClassGroup: string | null; +} + +export const useLinkSignature = ({ data, targetSystemClassGroup }: UseLinkSignatureProps) => { + const { + outCommand, + data: { systemSignatures, systems, wormholesData }, + } = useMapRootState(); + + const ref = useRef({ outCommand }); + ref.current = { outCommand }; + + const [userSettings, setUserSettings] = useState(null); + + useEffect(() => { + outCommand({ type: OutCommand.getUserSettings, data: null }) + .then((res: any) => setUserSettings(res?.user_settings)) + .catch((e: any) => console.warn('Failed to fetch user settings', e)); + }, [outCommand]); + + const handleLinkSignature = useCallback( + async (signature: SystemSignature) => { + const { outCommand } = ref.current; + + const sourceSystem = systems.find((s: any) => s.system_static_info?.solar_system_id === data.solar_system_source); + const systemUuid = sourceSystem?.id || data.solar_system_source.toString(); + + const targetSystem = systems.find((s: any) => s.system_static_info?.solar_system_id === data.solar_system_target); + const targetSystemUuid = targetSystem?.id; + const targetSolarSystemIdStr = data.solar_system_target?.toString(); + + const signatureToLink = { ...signature, group: SignatureGroup.Wormhole }; + + const { updatedSignature, shouldUpdate } = await handleAutoBookmark( + signatureToLink, + userSettings, + systemSignatures, + systemUuid, + data.solar_system_source.toString(), + wormholesData, + targetSystemClassGroup, + targetSystemUuid, + targetSolarSystemIdStr, + ); + + if (shouldUpdate) { + await outCommand({ + type: OutCommand.updateSignatures, + data: { + system_id: `${data.solar_system_source}`, + updated: [updatedSignature], + removed: [], + deleteTimeout: 0, + }, + }); + } + + await outCommand({ + type: OutCommand.linkSignatureToSystem, + data: { + ...data, + signature_eve_id: signature.eve_id, + }, + }); + + const systemAutoTag = userSettings?.system_auto_tag; + const systemCustomLabelName = userSettings?.system_custom_label_name; + + if (systemAutoTag || systemCustomLabelName) { + const info = parseSignatureCustomInfo(updatedSignature.custom_info); + + if (info.bookmark_index !== undefined) { + const bIndex = info.bookmark_index; + const startAtZero = userSettings?.bookmark_wormholes_start_at_zero; + const letter = numberToLetters(bIndex, startAtZero); + + if (targetSystem) { + if (systemAutoTag) { + let tagValue = ''; + switch (systemAutoTag) { + case 'index': + tagValue = bIndex.toString(); + break; + case 'chain_index': + tagValue = (info.bookmark_index_chained as string) || bIndex.toString(); + break; + case 'index_letter': + tagValue = letter; + break; + case 'chain_index_letters': + tagValue = (info.bookmark_index_chained_letters as string) || letter; + break; + } + + if (tagValue) { + await outCommand({ + type: OutCommand.updateSystemTag, + data: { + system_id: targetSystem.id, + value: tagValue, + }, + }); + } + } + + if (systemCustomLabelName) { + let labelValue = ''; + switch (systemCustomLabelName) { + case 'index': + labelValue = bIndex.toString(); + break; + case 'index_letter': + labelValue = letter; + break; + case 'chain_index': + labelValue = (info.bookmark_index_chained as string) || bIndex.toString(); + break; + case 'chain_index_letters': + labelValue = (info.bookmark_index_chained_letters as string) || letter; + break; + } + + if (labelValue) { + const outLabel = new LabelsManager(targetSystem.labels ?? ''); + outLabel.updateCustomLabel(labelValue); + + await outCommand({ + type: OutCommand.updateSystemLabels, + data: { + system_id: targetSystem.id, + value: outLabel.toString(), + }, + }); + } + } + } + } + } + }, + [data, userSettings, targetSystemClassGroup, systemSignatures, systems, wormholesData], + ); + + return { handleLinkSignature }; +}; 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 3943c0cf..62611b24 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettings.tsx @@ -11,6 +11,7 @@ import { } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx'; import { WidgetsSettings } from './components/WidgetsSettings'; import { CommonSettings } from './components/CommonSettings'; +import { BookmarksSettings } from './components/BookmarksSettings'; import { SettingsListItem } from './types.ts'; import { ImportExport } from './components/ImportExport.tsx'; import { ServerSettings } from './components/ServerSettings.tsx'; @@ -26,7 +27,7 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => { const [activeIndex, setActiveIndex] = useState(0); const { outCommand } = useMapRootState(); - const { renderSettingItem, setUserRemoteSettings } = useMapSettings(); + const { renderSettingItem, setUserRemoteSettings, settings } = useMapSettings(); const isAdmin = useMapCheckPermissions([UserPermission.ADMIN_MAP]); const refVars = useRef({ outCommand, onHide, visible }); @@ -62,15 +63,16 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => { header="Map user settings" visible draggable={false} - className="w-[600px] h-[400px]" + className="w-[600px] h-[460px]" + contentClassName="custom-scrollbar" onShow={handleShow} onHide={handleHide} > -
-
+
+
setActiveIndex(e.index)} > @@ -89,6 +91,10 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => { {renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)} + + + + diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx index 7b258617..2a05f55b 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx @@ -21,12 +21,15 @@ import { import { OutCommand } from '@/hooks/Mapper/types'; import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components'; import { Dropdown } from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { WithChildren } from '@/hooks/Mapper/types/common.ts'; type MapSettingsContextType = { renderSettingItem: (item: SettingsListItem) => ReactNode; + updateSetting: (prop: keyof UserSettings, value: boolean | string | Record) => Promise; setUserRemoteSettings: Dispatch>; + settings: UserSettings; }; const MapSettingsContext = createContext(undefined); @@ -51,29 +54,39 @@ export const MapSettingsProvider = ({ children }: WithChildren) => { const refVars = useRef({ mergedSettings, userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings }); refVars.current = { mergedSettings, userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings }; - const handleSettingChange = useCallback(async (prop: keyof UserSettings, value: boolean | string) => { - const { userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings } = refVars.current; + const handleSettingChange = useCallback( + async (prop: keyof UserSettings, value: boolean | string | Record) => { + const { userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings } = refVars.current; - if (UserSettingsRemoteList.includes(prop as any)) { - const newRemoteSettings = { - ...userRemoteSettings, - [prop]: value, - }; - await outCommand({ - type: OutCommand.updateUserSettings, - data: newRemoteSettings, - }); - setUserRemoteSettings(newRemoteSettings); - } else { - setInterfaceSettings({ - ...interfaceSettings, - [prop]: value, - }); - } - }, []); + if (UserSettingsRemoteList.includes(prop as any)) { + const newRemoteSettings = { + ...userRemoteSettings, + [prop]: value, + }; + await outCommand({ + type: OutCommand.updateUserSettings, + data: newRemoteSettings, + }); + setUserRemoteSettings(newRemoteSettings); + } else { + setInterfaceSettings({ + ...interfaceSettings, + [prop]: value, + }); + } + }, + [], + ); const renderSettingItem = useCallback( (item: SettingsListItem) => { + if (item.dependsOn) { + const dependsOnValue = refVars.current.mergedSettings[item.dependsOn]; + if (!dependsOnValue) { + return null; + } + } + const currentValue = refVars.current.mergedSettings[item.prop]; if (item.type === 'checkbox') { @@ -103,13 +116,30 @@ export const MapSettingsProvider = ({ children }: WithChildren) => { ); } + if (item.type === 'text') { + return ( +
+ {item.label && } + handleSettingChange(item.prop, e.target.value)} + placeholder={item.placeholder} + /> + {item.helperText && {item.helperText}} +
+ ); + } + return null; }, [handleSettingChange], ); return ( - + {children} ); diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/BookmarkNameFormatSetting.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/BookmarkNameFormatSetting.tsx new file mode 100644 index 00000000..11a60100 --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/BookmarkNameFormatSetting.tsx @@ -0,0 +1,352 @@ +import { useMapSettings } from '../MapSettingsProvider'; +import { UserSettingsRemoteProps } from '../types'; +import { InputText } from 'primereact/inputtext'; +import { WdButton } from '@/hooks/Mapper/components/ui-kit'; +import { useMemo, useState, useRef, useEffect, useCallback } from 'react'; +import { formatBookmarkName } from '@/hooks/Mapper/helpers/bookmarkFormatHelper'; +import { SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types'; +import { MassState, TimeStatus } from '@/hooks/Mapper/types/connection'; + +const DUMMY_SIG_BASE: SystemSignature = { + eve_id: 'ABC-123', + name: 'ABC-123', + kind: SignatureKind.CosmicSignature, + type: 'K162', + description: 'To Jita', + temporary_name: 'Temp', + group: SignatureGroup.Wormhole, + custom_info: '', +}; + +const VARIABLES = [ + { id: '{index}', desc: 'Numeric index (e.g., 1, 2, 3)' }, + { id: '{index_letter}', desc: 'Letter index (e.g., A, B, C)' }, + { id: '{chain_index}', desc: 'Numeric chain path (e.g., 11, 12, 121)' }, + { id: '{chain_index_letters}', desc: 'Letter chain path (e.g., A, A1, A21)' }, + { id: '{sig_letters}', desc: 'First 3 chars of signature (e.g., ABC)' }, + { id: '{sig}', desc: 'Full signature ID (e.g., ABC-123)' }, + { id: '{dest_type}', desc: 'Destination class (e.g., C5, HS, Thera)' }, + { + id: '{dest_class_index}', + desc: 'Letter index for multiple holes to same class (empty if only 1, otherwise a, b, c...)', + }, + { id: '{type}', desc: 'Wormhole type (e.g., K162, H900)' }, + { id: '{size}', desc: 'Hole size (e.g., S, M, XL)' }, + { id: '{mass}', desc: 'Total mass in bil (e.g., 3.3)' }, + { id: '{time_status}', desc: 'Time remaining (e.g., 1H, 4H, 16H)' }, + { id: '{mass_status}', desc: 'Mass remaining (e.g., Destab, Crit)' }, + { id: '{temporary_name}', desc: 'Temporary name if set' }, + { id: '{description}', desc: 'Custom description' }, +]; + +interface CustomMappingInputProps { + mappingKey: string; + label: string; + defaultVal: string; + localMapping: Record; + setLocalMapping: React.Dispatch>>; + updateSetting: (prop: any, value: any) => void; +} + +const CustomMappingInput = ({ + mappingKey, + label, + defaultVal, + localMapping, + setLocalMapping, + updateSetting, +}: CustomMappingInputProps) => { + const value = localMapping[mappingKey] !== undefined ? localMapping[mappingKey] : defaultVal; + + const handleChange = useCallback((e: React.ChangeEvent) => { + setLocalMapping(prev => { + const newMapping = { ...prev }; + newMapping[mappingKey] = e.target.value; + return newMapping; + }); + }, [mappingKey, setLocalMapping]); + + const handleBlur = useCallback((e: React.FocusEvent) => { + const val = e.target.value; + setLocalMapping(prev => { + const currentVal = prev[mappingKey] !== undefined ? prev[mappingKey] : defaultVal; + if (val === currentVal) return prev; + + const newMapping = { ...prev }; + if (val === defaultVal) { + delete newMapping[mappingKey]; + } else { + newMapping[mappingKey] = val; + } + updateSetting(UserSettingsRemoteProps.bookmark_custom_mapping, newMapping); + return newMapping; + }); + }, [mappingKey, defaultVal, setLocalMapping, updateSetting]); + + return ( +
+ + +
+ ); +}; + +const TIME_OPTIONS = [ + { key: 'time_1h', label: '1 Hour', defaultVal: '1H' }, + { key: 'time_4h', label: '4 Hours', defaultVal: '4H' }, + { key: 'time_4h30m', label: '4.5 Hours', defaultVal: '4.5H' }, + { key: 'time_16h', label: '16 Hours', defaultVal: '16H' }, + { key: 'time_24h', label: '24 Hours', defaultVal: '' }, + { key: 'time_48h', label: '48 Hours', defaultVal: '' }, +]; + +const MASS_OPTIONS = [ + { key: 'mass_normal', label: 'Normal Mass', defaultVal: '' }, + { key: 'mass_half', label: 'Destab', defaultVal: 'Destab' }, + { key: 'mass_verge', label: 'Critical', defaultVal: 'Crit' }, +]; + +const OTHER_OPTIONS = [{ key: 'chain_separator', label: 'Chain Separator', defaultVal: '' }]; + +const SIZE_OPTIONS = [ + { key: 'size_small', label: 'Small (Frigate)', defaultVal: 'S' }, + { key: 'size_medium', label: 'Medium', defaultVal: 'M' }, + { key: 'size_large', label: 'Large', defaultVal: '' }, + { key: 'size_freight', label: 'Huge / Freight', defaultVal: 'XL' }, + { key: 'size_capital', label: 'Capital', defaultVal: 'C' }, +]; + +const CLASS_OPTIONS = [ + { key: 'class_c1', label: 'Class 1', defaultVal: 'C1' }, + { key: 'class_c2', label: 'Class 2', defaultVal: 'C2' }, + { key: 'class_c3', label: 'Class 3', defaultVal: 'C3' }, + { key: 'class_c4', label: 'Class 4', defaultVal: 'C4' }, + { key: 'class_c5', label: 'Class 5', defaultVal: 'C5' }, + { key: 'class_c6', label: 'Class 6', defaultVal: 'C6' }, + { key: 'class_c13', label: 'Class 13', defaultVal: 'C13' }, + { key: 'class_c1c2c3', label: 'Class 1/2/3', defaultVal: 'C1/C2/C3' }, + { key: 'class_c4c5', label: 'Class 4/5', defaultVal: 'C4/C5' }, + { key: 'class_hs', label: 'High-Sec', defaultVal: 'HS' }, + { key: 'class_ls', label: 'Low-Sec', defaultVal: 'LS' }, + { key: 'class_ns', label: 'Null-Sec', defaultVal: 'NS' }, + { key: 'class_thera', label: 'Thera', defaultVal: 'Thera' }, + { key: 'class_pochven', label: 'Pochven', defaultVal: 'Pochven' }, + { key: 'class_drifter', label: 'Drifter', defaultVal: 'Drifter' }, +]; + +export const BookmarkNameFormatSetting = () => { + const { settings, updateSetting } = useMapSettings(); + const formatStr = settings.bookmark_name_format || ''; + const customMapping = settings.bookmark_custom_mapping || {}; + const inputRef = useRef(null); + + const [localFormat, setLocalFormat] = useState(formatStr); + const [localMapping, setLocalMapping] = useState(customMapping); + const [showAdvanced, setShowAdvanced] = useState(false); + + useEffect(() => { + setLocalFormat(formatStr); + }, [formatStr]); + + useEffect(() => { + setLocalMapping(customMapping); + }, [customMapping]); + + const preview = useMemo(() => { + const isZero = settings.bookmark_wormholes_start_at_zero; + const sep = localMapping?.chain_separator || ''; + + const chainNum = isZero ? `0${sep}0${sep}1` : `1${sep}1${sep}2`; + const chainLet = isZero ? `A${sep}0${sep}1` : `A${sep}1${sep}2`; + const currentIndex = isZero ? 1 : 2; + + const dummySig: SystemSignature = { + ...DUMMY_SIG_BASE, + type: 'V283', + custom_info: JSON.stringify({ + time_status: TimeStatus._1h, + mass_status: MassState.verge, + bookmark_index_chained: chainNum, + bookmark_index_chained_letters: chainLet, + }), + }; + + const otherDummySig: SystemSignature = { + ...DUMMY_SIG_BASE, + type: 'V283', + eve_id: 'DEF-456', + name: 'DEF-456', + custom_info: JSON.stringify({ + time_status: TimeStatus._1h, + mass_status: MassState.verge, + }), + }; + + const dummyWormholesData = { + V283: { + total_mass: 3300000000, + max_mass_per_jump: 62000000, + dest: ['hs'], + } as any, + }; + + return formatBookmarkName( + localFormat, + dummySig, + 'HS', + currentIndex, + dummyWormholesData, + isZero, + localMapping, + { preview_sys: [otherDummySig] }, + 'preview_sys', + ); + }, [localFormat, settings.bookmark_wormholes_start_at_zero, localMapping]); + + const handleBlur = useCallback(() => { + if (localFormat !== formatStr) { + updateSetting(UserSettingsRemoteProps.bookmark_name_format, localFormat); + } + }, [localFormat, formatStr, updateSetting]); + + const insertVariable = useCallback((variable: string) => { + const input = inputRef.current; + if (input) { + const start = input.selectionStart || 0; + const end = input.selectionEnd || 0; + const newFormat = localFormat.substring(0, start) + variable + localFormat.substring(end); + setLocalFormat(newFormat); + updateSetting(UserSettingsRemoteProps.bookmark_name_format, newFormat); + + setTimeout(() => { + input.focus(); + input.setSelectionRange(start + variable.length, start + variable.length); + }, 0); + } else { + const newFormat = localFormat + variable; + setLocalFormat(newFormat); + updateSetting(UserSettingsRemoteProps.bookmark_name_format, newFormat); + } + }, [localFormat, updateSetting]); + + const resetToDefault = useCallback(() => { + const defaultFormat = '{chain_index} {sig_letters} {dest_type} {size} {mass_status} {time_status}'; + setLocalFormat(defaultFormat); + updateSetting(UserSettingsRemoteProps.bookmark_name_format, defaultFormat); + }, [updateSetting]); + + const renderCustomMappingInputs = (options: { key: string; label: string; defaultVal: string }[]) => { + return options.map(opt => ( + + )); + }; + + return ( +
+
+ + + Reset to Default + +
+ setLocalFormat(e.target.value)} + onBlur={handleBlur} + placeholder="e.g. {chain_index} {sig_letters} {dest_type} {size} {mass_status} {time_status}" + /> +
+ Live Preview: + + {preview || Empty} + +
+ +
+

Available Variables (Click to insert)

+
    + {VARIABLES.map(v => ( +
  • + insertVariable(v.id)} + > + {v.id} + {' '} + - {v.desc} +
  • + ))} +
+
+ +
+ setShowAdvanced(!showAdvanced)} + > + {showAdvanced ? 'Hide Advanced String Customization' : 'Show Advanced String Customization'} + + + {showAdvanced && ( +
+
+

Override the default output of specific format variables.

+ { + setLocalMapping({}); + updateSetting(UserSettingsRemoteProps.bookmark_custom_mapping, {}); + }} + > + Reset Mappings + +
+ +
+
Time
+
{renderCustomMappingInputs(TIME_OPTIONS)}
+
+ +
+
Mass
+
{renderCustomMappingInputs(MASS_OPTIONS)}
+
+ +
+
Other / Formatting
+
{renderCustomMappingInputs(OTHER_OPTIONS)}
+
+ +
+
Hole Sizes
+
{renderCustomMappingInputs(SIZE_OPTIONS)}
+
+ +
+
Destination Classes
+
{renderCustomMappingInputs(CLASS_OPTIONS)}
+
+
+ )} +
+
+ ); +}; diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/BookmarksSettings.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/BookmarksSettings.tsx new file mode 100644 index 00000000..88cc6eeb --- /dev/null +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/BookmarksSettings.tsx @@ -0,0 +1,33 @@ +import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; +import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx'; +import { BOOKMARKS_SETTINGS_PROPS } from '../constants.ts'; +import { BookmarkNameFormatSetting } from './BookmarkNameFormatSetting'; + +export const BookmarksSettings = () => { + const { + storedSettings: { interfaceSettings, setInterfaceSettings }, + } = useMapRootState(); + const { renderSettingItem, settings } = useMapSettings(); + + return ( +
+ {!settings.link_signature_on_splash && !interfaceSettings.hideBookmarkWarning && ( +
+ ⚠️ It is highly recommended to enable 'Link signature on splash' (in the Signatures tab) to fully utilize + automatic bookmark naming. + +
+ )} + +
{BOOKMARKS_SETTINGS_PROPS.map(renderSettingItem)}
+ + +
+ ); +}; diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts index cf50f03b..078a08b9 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts @@ -6,12 +6,38 @@ export const DEFAULT_REMOTE_SETTINGS = { [UserSettingsRemoteProps.link_signature_on_splash]: false, [UserSettingsRemoteProps.select_on_spash]: false, [UserSettingsRemoteProps.delete_connection_with_sigs]: false, + [UserSettingsRemoteProps.bookmark_name_format]: '', + [UserSettingsRemoteProps.bookmark_custom_mapping]: {}, + [UserSettingsRemoteProps.bookmark_wormholes_start_at_zero]: false, + [UserSettingsRemoteProps.bookmark_auto_copy]: true, + [UserSettingsRemoteProps.bookmark_auto_temp_name]: '', + [UserSettingsRemoteProps.system_auto_tag]: '', + [UserSettingsRemoteProps.system_custom_label_name]: '', + [UserSettingsRemoteProps.bookmark_return_hole_ignore]: false, + [UserSettingsRemoteProps.bookmark_return_hole_symbol]: '', }; +export const AUTO_FORMAT_OPTIONS = [ + { label: 'Disabled', value: '' }, + { label: 'Numeric index (1, 2, 3)', value: 'index' }, + { label: 'Letter index (A, B, C)', value: 'index_letter' }, + { label: 'Numeric chain (11, 12, 121)', value: 'chain_index' }, + { label: 'Letter chain (A, A1, A21)', value: 'chain_index_letters' }, +]; + export const UserSettingsRemoteList = [ UserSettingsRemoteProps.link_signature_on_splash, UserSettingsRemoteProps.select_on_spash, UserSettingsRemoteProps.delete_connection_with_sigs, + UserSettingsRemoteProps.bookmark_name_format, + UserSettingsRemoteProps.bookmark_custom_mapping, + UserSettingsRemoteProps.bookmark_wormholes_start_at_zero, + UserSettingsRemoteProps.bookmark_auto_copy, + UserSettingsRemoteProps.bookmark_auto_temp_name, + UserSettingsRemoteProps.system_auto_tag, + UserSettingsRemoteProps.system_custom_label_name, + UserSettingsRemoteProps.bookmark_return_hole_ignore, + UserSettingsRemoteProps.bookmark_return_hole_symbol, ]; // export const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [ @@ -48,6 +74,48 @@ export const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [ }, ]; +export const BOOKMARKS_SETTINGS_PROPS: SettingsListItem[] = [ + { + prop: UserSettingsRemoteProps.bookmark_auto_copy, + label: 'Automatically copy bookmarks to clipboard', + type: 'checkbox', + }, + { + prop: UserSettingsRemoteProps.bookmark_wormholes_start_at_zero, + label: 'Start wormhole indices at 0', + type: 'checkbox', + }, + { + prop: UserSettingsRemoteProps.bookmark_return_hole_ignore, + label: 'Ignore return hole when creating indexes', + type: 'checkbox', + }, + { + prop: UserSettingsRemoteProps.bookmark_return_hole_symbol, + label: 'Return hole symbol (use space for empty)', + type: 'text', + dependsOn: UserSettingsRemoteProps.bookmark_return_hole_ignore, + }, + { + prop: UserSettingsRemoteProps.bookmark_auto_temp_name, + label: 'Auto-fill wormhole temporary name', + type: 'dropdown', + options: AUTO_FORMAT_OPTIONS, + }, + { + prop: UserSettingsRemoteProps.system_auto_tag, + label: 'Auto-tag jumped system', + type: 'dropdown', + options: AUTO_FORMAT_OPTIONS, + }, + { + prop: UserSettingsRemoteProps.system_custom_label_name, + label: 'Auto-label jumped system', + type: 'dropdown', + options: AUTO_FORMAT_OPTIONS, + }, +]; + export const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [ { prop: UserSettingsRemoteProps.delete_connection_with_sigs, diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts index 30d76809..196814fb 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts @@ -4,12 +4,30 @@ export enum UserSettingsRemoteProps { link_signature_on_splash = 'link_signature_on_splash', select_on_spash = 'select_on_spash', delete_connection_with_sigs = 'delete_connection_with_sigs', + bookmark_name_format = 'bookmark_name_format', + bookmark_custom_mapping = 'bookmark_custom_mapping', + bookmark_wormholes_start_at_zero = 'bookmark_wormholes_start_at_zero', + bookmark_auto_copy = 'bookmark_auto_copy', + bookmark_auto_temp_name = 'bookmark_auto_temp_name', + system_auto_tag = 'system_auto_tag', + system_custom_label_name = 'system_custom_label_name', + bookmark_return_hole_ignore = 'bookmark_return_hole_ignore', + bookmark_return_hole_symbol = 'bookmark_return_hole_symbol', } export type UserSettingsRemote = { link_signature_on_splash: boolean; select_on_spash: boolean; delete_connection_with_sigs: boolean; + bookmark_name_format: string; + bookmark_custom_mapping?: Record; + bookmark_wormholes_start_at_zero: boolean; + bookmark_auto_copy: boolean; + bookmark_auto_temp_name: string; + system_auto_tag: string; + system_custom_label_name: string; + bookmark_return_hole_ignore: boolean; + bookmark_return_hole_symbol: string; }; export type UserSettings = UserSettingsRemote & InterfaceStoredSettings; @@ -17,6 +35,9 @@ export type UserSettings = UserSettingsRemote & InterfaceStoredSettings; export type SettingsListItem = { prop: keyof UserSettings; label: string; - type: 'checkbox' | 'dropdown'; + type: 'checkbox' | 'dropdown' | 'text'; options?: { label: string; value: string }[]; + placeholder?: string; + helperText?: string; + dependsOn?: keyof UserSettings; }; diff --git a/assets/js/hooks/Mapper/components/mapRootContent/components/SignatureSettings/SignatureSettings.tsx b/assets/js/hooks/Mapper/components/mapRootContent/components/SignatureSettings/SignatureSettings.tsx index 30ef9f8e..4cd3b6cb 100644 --- a/assets/js/hooks/Mapper/components/mapRootContent/components/SignatureSettings/SignatureSettings.tsx +++ b/assets/js/hooks/Mapper/components/mapRootContent/components/SignatureSettings/SignatureSettings.tsx @@ -2,18 +2,22 @@ import { SignatureGroupContent, SignatureGroupSelect, } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components'; +import { getSystemClassGroup } from '@/hooks/Mapper/components/map/helpers/getSystemClassGroup.ts'; import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx'; import { WdButton } from '@/hooks/Mapper/components/ui-kit'; +import { handleAutoBookmark } from '@/hooks/Mapper/helpers/bookmarkFormatHelper.ts'; +import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { MassState, OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types'; import { Dialog } from 'primereact/dialog'; import { InputText } from 'primereact/inputtext'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; type SystemSignaturePrepared = Omit & { linked_system: string; destType: string; + k162Type?: string; time_status: TimeStatus; mass_status: MassState; }; @@ -26,11 +30,23 @@ export interface MapSettingsProps { } export const SignatureSettings = ({ systemId, show, onHide, signatureData }: MapSettingsProps) => { - const { outCommand } = useMapRootState(); + const { + outCommand, + data: { systemSignatures, systems, wormholesData }, + } = useMapRootState(); const handleShow = async () => {}; + const signatureForm = useForm>({}); + const [userSettings, setUserSettings] = useState(null); + + useEffect(() => { + outCommand({ type: OutCommand.getUserSettings, data: null }) + .then((res: any) => setUserSettings(res?.user_settings)) + .catch((e: any) => console.warn('Failed to fetch user settings', e)); + }, [outCommand]); + const handleSave = useCallback( // TODO: need fix async (e: any) => { @@ -44,10 +60,13 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map switch (group) { case SignatureGroup.Wormhole: + const existingInfo = parseSignatureCustomInfo(signatureData.custom_info); out = { ...out, custom_info: JSON.stringify({ + ...existingInfo, destType: values.destType, + k162Type: values.k162Type, time_status: values.time_status, mass_status: values.mass_status, }), @@ -103,6 +122,45 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map // Note: despite groups have optional type - this will always set out = { ...out, group: group! }; + if (group === SignatureGroup.Wormhole) { + let targetSystem = null; + if (values.linked_system) { + targetSystem = systems.find((s: any) => s.system_static_info?.solar_system_id?.toString() === values.linked_system); + } + + let targetSystemClassGroup = null; + if (targetSystem?.system_static_info) { + targetSystemClassGroup = getSystemClassGroup(targetSystem.system_static_info.system_class); + } + + const targetSystemUuid = targetSystem?.id; + + let targetSolarSystemIdStr = values.linked_system; + if (targetSystem?.system_static_info?.solar_system_id) { + targetSolarSystemIdStr = targetSystem.system_static_info.solar_system_id.toString(); + } + + const currentSystem = systems.find((s: any) => s.id === systemId); + + let solarSystemIdStr = systemId; + if (currentSystem?.system_static_info?.solar_system_id) { + solarSystemIdStr = currentSystem.system_static_info.solar_system_id.toString(); + } + + const { updatedSignature } = await handleAutoBookmark( + out, + userSettings, + systemSignatures, + systemId, + solarSystemIdStr, + wormholesData, + targetSystemClassGroup, + targetSystemUuid, + targetSolarSystemIdStr, + ); + out = updatedSignature; + } + await outCommand({ type: OutCommand.updateSignatures, data: { @@ -131,7 +189,17 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map signatureForm.reset(); onHide(); }, - [signatureData, signatureForm, outCommand, systemId, onHide], + [ + signatureData, + signatureForm, + outCommand, + systemId, + onHide, + systemSignatures, + systems, + wormholesData, + userSettings, + ], ); useEffect(() => { @@ -142,19 +210,22 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map const { linked_system, custom_info, ...rest } = signatureData; - let destType = null; + let destType: string | undefined = undefined; + let k162Type: string | undefined = undefined; let time_status = TimeStatus._24h; let mass_status = MassState.normal; if (custom_info) { - const customInfo = JSON.parse(custom_info); + const customInfo = parseSignatureCustomInfo(custom_info); destType = customInfo.destType; - time_status = customInfo.time_status; + k162Type = customInfo.k162Type; + time_status = customInfo.time_status ?? TimeStatus._24h; mass_status = customInfo.mass_status ?? MassState.normal; } signatureForm.reset({ linked_system: linked_system?.solar_system_id.toString() ?? undefined, destType: destType, + k162Type: k162Type, time_status: time_status, mass_status: mass_status, ...rest, diff --git a/assets/js/hooks/Mapper/helpers/bookmarkFormatHelper.ts b/assets/js/hooks/Mapper/helpers/bookmarkFormatHelper.ts new file mode 100644 index 00000000..6849a536 --- /dev/null +++ b/assets/js/hooks/Mapper/helpers/bookmarkFormatHelper.ts @@ -0,0 +1,491 @@ +import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types'; +import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo'; +import { MassState, TimeStatus } from '@/hooks/Mapper/types/connection'; +import { getSystemClassGroup } from '@/hooks/Mapper/components/map/helpers/getSystemClassGroup'; +import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes'; +import { + WORMHOLES_ADDITIONAL_INFO, + SHIP_MASSES_SIZE, + SHIP_SIZES_NAMES_SHORT, +} from '@/hooks/Mapper/components/map/constants'; +import { ALL_DEST_TYPES_MAP, MULTI_DEST_WHS } from '@/hooks/Mapper/constants'; +import { ShipSizeStatus } from '@/hooks/Mapper/types/connection'; + +const getTimeStatusString = (status?: TimeStatus, mapping?: Record): string => { + switch (status) { + case TimeStatus._1h: + return mapping?.time_1h !== undefined ? mapping.time_1h : '1H'; + case TimeStatus._4h: + return mapping?.time_4h !== undefined ? mapping.time_4h : '4H'; + case TimeStatus._4h30m: + return mapping?.time_4h30m !== undefined ? mapping.time_4h30m : '4.5H'; + case TimeStatus._16h: + return mapping?.time_16h !== undefined ? mapping.time_16h : '16H'; + case TimeStatus._24h: + return mapping?.time_24h !== undefined ? mapping.time_24h : ''; + case TimeStatus._48h: + return mapping?.time_48h !== undefined ? mapping.time_48h : ''; + default: + return ''; + } +}; + +const getMassStatusString = (status?: MassState, mapping?: Record): string => { + switch (status) { + case MassState.normal: + return mapping?.mass_normal !== undefined ? mapping.mass_normal : ''; + case MassState.half: + return mapping?.mass_half !== undefined ? mapping.mass_half : 'Destab'; + case MassState.verge: + return mapping?.mass_verge !== undefined ? mapping.mass_verge : 'Crit'; + default: + return ''; + } +}; + +const DEST_CLASS_OVERRIDES: Record = { + h: 'HS', + hs: 'HS', + 'hi-sec': 'HS', + l: 'LS', + ls: 'LS', + 'low-sec': 'LS', + n: 'NS', + ns: 'NS', + 'null-sec': 'NS', + t: 'Thera', + thera: 'Thera', + d: 'Drifter', + drifter: 'Drifter', + p: 'Pochven', + pochven: 'Pochven', + 'c1/c2/c3': 'C1/C2/C3', + 'c4/c5': 'C4/C5', +}; + +const formatDestString = (dest: string | null | undefined, mapping?: Record): string => { + if (!dest) return '?'; + const lowerDest = dest.toLowerCase(); + + const normalizedDest = DEST_CLASS_OVERRIDES[lowerDest] ? DEST_CLASS_OVERRIDES[lowerDest].toLowerCase() : lowerDest; + const mappingKey = `class_${normalizedDest.replace(/[^a-z0-9]/g, '')}`; + if (mapping && mapping[mappingKey] !== undefined) { + return mapping[mappingKey]; + } + + if (DEST_CLASS_OVERRIDES[lowerDest]) { + return DEST_CLASS_OVERRIDES[lowerDest]; + } + if (lowerDest.length <= 3) return dest.toUpperCase(); + return dest.charAt(0).toUpperCase() + dest.slice(1); +}; + +export const numberToLetters = (num: number, startAtZero: boolean = false): string => { + if (startAtZero) { + num += 1; + } + let letters = ''; + while (num > 0) { + const mod = (num - 1) % 26; + letters = String.fromCharCode(65 + mod) + letters; + num = Math.floor((num - mod) / 26); + } + return letters; +}; + +export const calculateBookmarkIndex = ( + systemSignatures: Record, + currentSystemUuid: string, + currentSolarSystemId: string, + currentEveId: string, + startAtZero: boolean = false, + separator: string = '', +): { index: number; chained: string; chainedLetters: string } => { + let parentBookmarkIndex: string | undefined; + let parentBookmarkIndexLetters: string | undefined; + + for (const [sysId, sigs] of Object.entries(systemSignatures)) { + if (sysId === currentSystemUuid || sysId === currentSolarSystemId) continue; + + const parentSigs = sigs.filter(sig => sig.linked_system?.solar_system_id?.toString() === currentSolarSystemId); + for (const parentSig of parentSigs) { + const parentInfo = parseSignatureCustomInfo(parentSig.custom_info); + + // Return holes have their bookmark_index deleted, so we skip them to avoid hijacking the chain + if (parentInfo.bookmark_index === undefined) continue; + + if (parentInfo.bookmark_index_chained != null) { + if (!parentBookmarkIndex || String(parentInfo.bookmark_index_chained).length < parentBookmarkIndex.length) { + parentBookmarkIndex = String(parentInfo.bookmark_index_chained); + } + } else if (parentInfo.bookmark_index != null) { + if (!parentBookmarkIndex || String(parentInfo.bookmark_index).length < parentBookmarkIndex.length) { + parentBookmarkIndex = String(parentInfo.bookmark_index); + } + } + + if (parentInfo.bookmark_index_chained_letters != null) { + if ( + !parentBookmarkIndexLetters || + String(parentInfo.bookmark_index_chained_letters).length < parentBookmarkIndexLetters.length + ) { + parentBookmarkIndexLetters = String(parentInfo.bookmark_index_chained_letters); + } + } + } + } + + const currentSigsRaw = [ + ...(systemSignatures[currentSystemUuid] || []), + ...(systemSignatures[currentSolarSystemId] || []), + ]; + + // Deduplicate in case both keys map to the same or overlapping arrays + const uniqueCurrentSigs = Array.from(new Map(currentSigsRaw.map(sig => [sig.eve_id, sig])).values()); + + const existingIndices = uniqueCurrentSigs + .filter(sig => sig.eve_id !== currentEveId) + .map(sig => parseSignatureCustomInfo(sig.custom_info).bookmark_index) + .filter((i): i is number => typeof i === 'number' && i >= 0); + + let i = startAtZero ? 0 : 1; + while (existingIndices.includes(i)) { + i++; + } + + const chained = parentBookmarkIndex !== undefined ? `${parentBookmarkIndex}${separator}${i}` : `${i}`; + const chainedLetters = + parentBookmarkIndexLetters !== undefined + ? `${parentBookmarkIndexLetters}${separator}${i}` + : numberToLetters(i, startAtZero); + + return { index: i, chained, chainedLetters }; +}; + +export const formatBookmarkName = ( + formatStr: string, + signature: SystemSignature, + destSystemClass: string | null, + bookmarkIndex: number | string, + wormholesData: Record = {}, + startAtZero: boolean = false, + mapping?: Record, + systemSignatures?: Record, + currentSystemId?: string, + currentSolarSystemId?: string, +): string => { + let result = formatStr; + const info = parseSignatureCustomInfo(signature.custom_info); + + // Replace {index} + result = result.replace(/\{index\}/g, () => bookmarkIndex.toString()); + + // Replace {chain_index} + result = result.replace(/\{chain_index\}/g, () => info.bookmark_index_chained || bookmarkIndex.toString()); + + // Replace {index_letter} + result = result.replace(/\{index_letter\}/g, () => + typeof bookmarkIndex === 'number' ? numberToLetters(bookmarkIndex, startAtZero) : bookmarkIndex.toString(), + ); + + // Replace {chain_index_letters} + result = result.replace( + /\{chain_index_letters\}/g, + () => + info.bookmark_index_chained_letters || + info.bookmark_index_chained || + (typeof bookmarkIndex === 'number' ? numberToLetters(bookmarkIndex, startAtZero) : bookmarkIndex.toString()), + ); + + // Replace {sig_letters} (first 3 chars of eve_id) + const sigLetters = signature.eve_id.substring(0, 3).toUpperCase(); + result = result.replace(/\{sig_letters\}/g, () => sigLetters); + + // Replace {sig} (full signature ID) + const fullSig = signature.eve_id.toUpperCase(); + result = result.replace(/\{sig\}/g, () => fullSig); + + // Replace {dest_type} + let destTypeStr = ''; + if (destSystemClass) { + destTypeStr = destSystemClass; + } else if (signature.type && MULTI_DEST_WHS.includes(signature.type) && info.destType) { + const destOption = ALL_DEST_TYPES_MAP[info.destType]; + if (destOption) { + destTypeStr = destOption.label; + } + } else if (signature.type === 'K162' && info.k162Type) { + const k162Option = ALL_DEST_TYPES_MAP[info.k162Type]; + if (k162Option) { + destTypeStr = k162Option.label; + } + } else if (signature.type && wormholesData[signature.type]) { + const whData = wormholesData[signature.type]; + const whClass = whData?.dest?.length === 1 ? WORMHOLES_ADDITIONAL_INFO[whData.dest[0]] : null; + if (whClass) { + destTypeStr = whClass.shortName || whClass.shortTitle; + } + } else if (info.destType) { + const destOption = ALL_DEST_TYPES_MAP[info.destType]; + destTypeStr = destOption ? destOption.label : info.destType; + } + const finalDestTypeStr = formatDestString(destTypeStr, mapping); + result = result.replace(/\{dest_type\}/g, () => (finalDestTypeStr !== '?' ? finalDestTypeStr : '')); + + // Calculate {dest_class_index} + let destClassIndexStr = ''; + if ( + result.includes('{dest_class_index}') && + systemSignatures && + (currentSystemId || currentSolarSystemId) && + destTypeStr + ) { + const currentSigsRaw = [ + ...(systemSignatures[currentSystemId || ''] || []), + ...(systemSignatures[currentSolarSystemId || ''] || []), + ]; + // Deduplicate and ensure current signature is included + const sigsMap = new Map(currentSigsRaw.map(sig => [sig.eve_id, sig])); + sigsMap.set(signature.eve_id, signature); + const sigsInSystem = Array.from(sigsMap.values()); + + // Helper to get a simplified comparable class for a signature + const getSigDestClass = (sig: SystemSignature) => { + if (sig.eve_id === signature.eve_id) return finalDestTypeStr; + + const sigInfo = parseSignatureCustomInfo(sig.custom_info); + let sDestTypeStr = ''; + if (sig.type && MULTI_DEST_WHS.includes(sig.type) && sigInfo.destType) { + const destOption = ALL_DEST_TYPES_MAP[sigInfo.destType]; + if (destOption) sDestTypeStr = destOption.label; + } else if (sig.type === 'K162' && sigInfo.k162Type) { + const k162Option = ALL_DEST_TYPES_MAP[sigInfo.k162Type]; + if (k162Option) sDestTypeStr = k162Option.label; + } else if (sig.type && wormholesData[sig.type]) { + const whData = wormholesData[sig.type]; + const whClass = whData?.dest?.length === 1 ? WORMHOLES_ADDITIONAL_INFO[whData.dest[0]] : null; + if (whClass) sDestTypeStr = whClass.shortName || whClass.shortTitle; + } else if (sigInfo.destType) { + const destOption = ALL_DEST_TYPES_MAP[sigInfo.destType]; + sDestTypeStr = destOption ? destOption.label : sigInfo.destType; + } + return formatDestString(sDestTypeStr, mapping); + }; + + const sameClassSigs = sigsInSystem.filter(s => { + if (s.group !== SignatureGroup.Wormhole) return false; + return getSigDestClass(s) === finalDestTypeStr; + }); + + // Sort by eve_id to ensure consistent ordering across clients + sameClassSigs.sort((a, b) => a.eve_id.localeCompare(b.eve_id)); + + const indexInClass = sameClassSigs.findIndex(s => s.eve_id === signature.eve_id); + if (indexInClass > 0 || (indexInClass === 0 && sameClassSigs.length > 1)) { + destClassIndexStr = String.fromCharCode(97 + indexInClass); // 0->a, 1->b, 2->c... + } + } + result = result.replace(/\{dest_class_index\}/g, () => destClassIndexStr); + + // Replace {size} and {mass} + let sizeStr = ''; + let massStr = ''; + let whDataForSize: WormholeDataRaw | null = null; + + if (signature.type && MULTI_DEST_WHS.includes(signature.type) && info.destType) { + const destOption = ALL_DEST_TYPES_MAP[info.destType]; + if (destOption && destOption.whClassName) { + const whName = destOption.whClassName.split('_')[0]; + whDataForSize = wormholesData[whName]; + } + } else if (signature.type === 'K162' && info.k162Type) { + const k162Option = ALL_DEST_TYPES_MAP[info.k162Type]; + if (k162Option && k162Option.whClassName) { + const whName = k162Option.whClassName.split('_')[0]; + whDataForSize = wormholesData[whName]; + } + } else if (signature.type && wormholesData[signature.type]) { + whDataForSize = wormholesData[signature.type]; + } + + if (whDataForSize) { + if (whDataForSize.max_mass_per_jump) { + const sizeStatus = SHIP_MASSES_SIZE[whDataForSize.max_mass_per_jump] ?? ShipSizeStatus.large; + const defaultSizeNames: Record = { + [ShipSizeStatus.small]: 'S', + [ShipSizeStatus.medium]: 'M', + [ShipSizeStatus.large]: '', + [ShipSizeStatus.freight]: 'XL', + [ShipSizeStatus.capital]: 'C', + }; + const sizeMappingKeys: Record = { + [ShipSizeStatus.small]: 'size_small', + [ShipSizeStatus.medium]: 'size_medium', + [ShipSizeStatus.large]: 'size_large', + [ShipSizeStatus.freight]: 'size_freight', + [ShipSizeStatus.capital]: 'size_capital', + }; + const mappingKey = sizeMappingKeys[sizeStatus]; + if (mapping && mapping[mappingKey] !== undefined) { + sizeStr = mapping[mappingKey]; + } else { + sizeStr = defaultSizeNames[sizeStatus] ?? SHIP_SIZES_NAMES_SHORT[sizeStatus] ?? ''; + } + } + if (whDataForSize.total_mass) { + massStr = Number((whDataForSize.total_mass / 1_000_000_000).toFixed(2)).toString(); + } + } + result = result.replace(/\{size\}/g, () => sizeStr); + result = result.replace(/\{mass\}/g, () => massStr); + + // Replace {type} -> signature.type + result = result.replace(/\{type\}/g, () => signature.type || ''); + + // Replace {time_status} -> Parsed from custom_info.time_status + result = result.replace(/\{time_status\}/g, () => getTimeStatusString(info.time_status, mapping)); + + // Replace {mass_status} -> Parsed from custom_info.mass_status + result = result.replace(/\{mass_status\}/g, () => getMassStatusString(info.mass_status, mapping)); + + // Replace {temporary_name} -> signature.temporary_name + result = result.replace(/\{temporary_name\}/g, () => signature.temporary_name || ''); + + // Replace {description} -> signature.description + result = result.replace(/\{description\}/g, () => signature.description || ''); + + // Cleanup whitespace + return result.trim().replace(/\s+/g, ' '); +}; + +export const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch (err) { + console.warn('Failed to copy to clipboard', err); + } +}; + +export const handleAutoBookmark = async ( + signature: SystemSignature, + currentSettings: any, + systemSignatures: Record, + currentSystemId: string, + currentSolarSystemId: string, + wormholesData: Record, + targetSystemClassGroup: string | null, + targetSystemUuid?: string, + targetSolarSystemId?: string, +): Promise<{ updatedSignature: SystemSignature; shouldUpdate: boolean }> => { + let updatedSignature = signature; + let shouldUpdate = false; + + if ( + signature.group !== SignatureGroup.Wormhole || + (!currentSettings?.bookmark_name_format && !currentSettings?.bookmark_auto_temp_name) + ) { + return { updatedSignature, shouldUpdate }; + } + + const info = parseSignatureCustomInfo(signature.custom_info); + let bookmarkIndex = info.bookmark_index; + let bookmarkIndexToUse: number | string = bookmarkIndex != null ? bookmarkIndex : ''; + + let isReturnHole = false; + let symbol = ''; + + if (currentSettings?.bookmark_return_hole_ignore && (targetSystemUuid || targetSolarSystemId)) { + const targetSigsRaw = [ + ...(targetSystemUuid ? systemSignatures[targetSystemUuid] || [] : []), + ...(targetSolarSystemId ? systemSignatures[targetSolarSystemId] || [] : []), + ]; + + const uniqueTargetSigs = Array.from(new Map(targetSigsRaw.map(sig => [sig.eve_id, sig])).values()); + + isReturnHole = uniqueTargetSigs.some( + sig => sig.linked_system?.solar_system_id?.toString() === currentSolarSystemId.toString(), + ); + + if (isReturnHole) { + symbol = currentSettings.bookmark_return_hole_symbol || ''; + if (symbol === ' ') symbol = ''; + } + } + + if (isReturnHole) { + if (info.bookmark_index !== undefined) { + delete info.bookmark_index; + } + info.bookmark_index_chained = symbol; + info.bookmark_index_chained_letters = symbol; + bookmarkIndexToUse = symbol; + updatedSignature = { ...signature, custom_info: JSON.stringify(info) }; + shouldUpdate = true; + } else if (bookmarkIndex == null) { + const separator = currentSettings?.bookmark_custom_mapping?.chain_separator || ''; + const calculated = calculateBookmarkIndex( + systemSignatures, + currentSystemId, + currentSolarSystemId, + signature.eve_id, + currentSettings?.bookmark_wormholes_start_at_zero, + separator, + ); + bookmarkIndex = calculated.index; + info.bookmark_index = calculated.index; + info.bookmark_index_chained = calculated.chained; + info.bookmark_index_chained_letters = calculated.chainedLetters; + bookmarkIndexToUse = calculated.index; + updatedSignature = { ...signature, custom_info: JSON.stringify(info) }; + shouldUpdate = true; + } + + const needsTempNameUpdate = + !updatedSignature.temporary_name || + (isReturnHole && updatedSignature.temporary_name !== symbol && currentSettings?.bookmark_auto_temp_name); + + if (currentSettings?.bookmark_auto_temp_name && needsTempNameUpdate) { + let autoName = ''; + switch (currentSettings.bookmark_auto_temp_name) { + case 'index': + autoName = bookmarkIndexToUse.toString(); + break; + case 'index_letter': + autoName = + typeof bookmarkIndexToUse === 'number' + ? numberToLetters(bookmarkIndexToUse, currentSettings.bookmark_wormholes_start_at_zero) + : bookmarkIndexToUse.toString(); + break; + case 'chain_index': + autoName = info.bookmark_index_chained || bookmarkIndexToUse.toString(); + break; + case 'chain_index_letters': + autoName = info.bookmark_index_chained_letters || info.bookmark_index_chained || bookmarkIndexToUse.toString(); + break; + } + if (autoName !== '' || isReturnHole) { + updatedSignature = { ...updatedSignature, temporary_name: autoName }; + shouldUpdate = true; + } + } + + if (currentSettings?.bookmark_name_format && currentSettings?.bookmark_auto_copy !== false) { + const formattedStr = formatBookmarkName( + currentSettings.bookmark_name_format, + updatedSignature, + targetSystemClassGroup, + bookmarkIndexToUse, + wormholesData, + currentSettings.bookmark_wormholes_start_at_zero, + currentSettings.bookmark_custom_mapping, + systemSignatures, + currentSystemId, + currentSolarSystemId, + ); + + // Run this synchronously to avoid clipboard issues if possible + await copyToClipboard(formattedStr); + } + + return { updatedSignature, shouldUpdate }; +}; diff --git a/assets/js/hooks/Mapper/helpers/parseSignatureCustomInfo.ts b/assets/js/hooks/Mapper/helpers/parseSignatureCustomInfo.ts index 0bb2b752..2e29c3e0 100644 --- a/assets/js/hooks/Mapper/helpers/parseSignatureCustomInfo.ts +++ b/assets/js/hooks/Mapper/helpers/parseSignatureCustomInfo.ts @@ -5,5 +5,10 @@ export const parseSignatureCustomInfo = (str: string | undefined): SignatureCust return {}; } - return JSON.parse(str); + try { + return JSON.parse(str); + } catch (e) { + console.warn('Failed to parse signature custom_info', e); + return {}; + } }; diff --git a/assets/js/hooks/Mapper/mapRootProvider/constants.ts b/assets/js/hooks/Mapper/mapRootProvider/constants.ts index f103efb5..4a2c4a5e 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/constants.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/constants.ts @@ -22,6 +22,7 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = { theme: AvailableThemes.default, pingsPlacement: PingsPlacement.rightTop, minimapPlacement: MiniMapPlacement.rightBottom, + hideBookmarkWarning: false, }; export const DEFAULT_ROUTES_SETTINGS: RoutesType = { diff --git a/assets/js/hooks/Mapper/mapRootProvider/migrations/list/index.ts b/assets/js/hooks/Mapper/mapRootProvider/migrations/list/index.ts index 882adaa6..e000151f 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/migrations/list/index.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/migrations/list/index.ts @@ -1,6 +1,7 @@ import { to_1 } from './to_1.ts'; import { to_2 } from './to_2.ts'; import { to_3 } from './to_3.ts'; +import { to_4 } from './to_4.ts'; import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts'; -export default [to_1, to_2, to_3] as MigrationStructure[]; +export default [to_1, to_2, to_3, to_4] as MigrationStructure[]; diff --git a/assets/js/hooks/Mapper/mapRootProvider/migrations/list/to_4.ts b/assets/js/hooks/Mapper/mapRootProvider/migrations/list/to_4.ts new file mode 100644 index 00000000..6d49c7a5 --- /dev/null +++ b/assets/js/hooks/Mapper/mapRootProvider/migrations/list/to_4.ts @@ -0,0 +1,28 @@ +import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts'; +import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/constants.ts'; + +export const to_4: MigrationStructure = { + to: 4, + up: (prev: any) => { + let hideBookmarkWarning = false; + + // Check if the old unmanaged setting exists + const unmanagedSetting = localStorage.getItem('hide_bookmark_warning'); + if (unmanagedSetting) { + hideBookmarkWarning = unmanagedSetting === 'true'; + localStorage.removeItem('hide_bookmark_warning'); + } + + const interfaceSettings = prev?.interface || {}; + + return { + ...prev, + interface: { + ...STORED_INTERFACE_DEFAULT_VALUES, + ...interfaceSettings, + // Carry over the unmanaged setting if it's true, or just use what interfaceSettings has + hideBookmarkWarning: hideBookmarkWarning || interfaceSettings.hideBookmarkWarning || false, + }, + }; + }, +}; diff --git a/assets/js/hooks/Mapper/mapRootProvider/types.ts b/assets/js/hooks/Mapper/mapRootProvider/types.ts index 290b4fc5..fa2a32dd 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/types.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/types.ts @@ -31,6 +31,7 @@ export type InterfaceStoredSettings = { theme: AvailableThemes; minimapPlacement: MiniMapPlacement; pingsPlacement: PingsPlacement; + hideBookmarkWarning: boolean; }; export type RoutesType = { @@ -47,13 +48,7 @@ export type RoutesType = { avoid: number[]; }; -export type RoutesByCategoryType = - | 'blueLoot' - | 'redLoot' - | 'thera' - | 'turnur' - | 'so_cleaning' - | 'trade_hubs'; +export type RoutesByCategoryType = 'blueLoot' | 'redLoot' | 'thera' | 'turnur' | 'so_cleaning' | 'trade_hubs'; export type RoutesByScopeType = 'ALL' | 'HIGH'; diff --git a/assets/js/hooks/Mapper/mapRootProvider/version.ts b/assets/js/hooks/Mapper/mapRootProvider/version.ts index 2d56ea8f..a9596f07 100644 --- a/assets/js/hooks/Mapper/mapRootProvider/version.ts +++ b/assets/js/hooks/Mapper/mapRootProvider/version.ts @@ -1,4 +1,4 @@ -export const STORED_SETTINGS_VERSION = 3; +export const STORED_SETTINGS_VERSION = 4; export const LS_KEY_LEGASY = 'map-user-settings'; export const LS_KEY = 'map-user-settings-v3'; diff --git a/assets/js/hooks/Mapper/types/signatures.ts b/assets/js/hooks/Mapper/types/signatures.ts index 6a11cc7d..eb68d693 100644 --- a/assets/js/hooks/Mapper/types/signatures.ts +++ b/assets/js/hooks/Mapper/types/signatures.ts @@ -29,9 +29,13 @@ export type GroupType = { export type SignatureCustomInfo = { destType?: string; + k162Type?: string; time_status?: number; isCrit?: boolean; mass_status?: number; + bookmark_index?: number; + bookmark_index_chained?: string; + bookmark_index_chained_letters?: string; }; export type SystemSignature = { diff --git a/lib/wanderer_app/repositories/map_user_settings_repo.ex b/lib/wanderer_app/repositories/map_user_settings_repo.ex index d0d5a00e..babbec42 100644 --- a/lib/wanderer_app/repositories/map_user_settings_repo.ex +++ b/lib/wanderer_app/repositories/map_user_settings_repo.ex @@ -5,7 +5,13 @@ defmodule WandererApp.MapUserSettingsRepo do "select_on_spash" => false, "link_signature_on_splash" => false, "delete_connection_with_sigs" => false, - "primary_character_id" => nil + "primary_character_id" => nil, + "bookmark_name_format" => "", + "bookmark_custom_mapping" => %{}, + "system_auto_tag" => "", + "system_custom_label_name" => "", + "bookmark_return_hole_ignore" => false, + "bookmark_return_hole_symbol" => "" } def get(map_id, user_id) do diff --git a/lib/wanderer_app_web/live/map/event_handlers/map_core_event_handler.ex b/lib/wanderer_app_web/live/map/event_handlers/map_core_event_handler.ex index 8b25a660..60db6be6 100644 --- a/lib/wanderer_app_web/live/map/event_handlers/map_core_event_handler.ex +++ b/lib/wanderer_app_web/live/map/event_handlers/map_core_event_handler.ex @@ -232,7 +232,20 @@ defmodule WandererAppWeb.MapCoreEventHandler do ) do settings = user_settings_form - |> Map.take(["select_on_spash", "link_signature_on_splash", "delete_connection_with_sigs"]) + |> Map.take([ + "select_on_spash", + "link_signature_on_splash", + "delete_connection_with_sigs", + "bookmark_name_format", + "bookmark_custom_mapping", + "bookmark_wormholes_start_at_zero", + "bookmark_auto_copy", + "bookmark_auto_temp_name", + "system_auto_tag", + "system_custom_label_name", + "bookmark_return_hole_ignore", + "bookmark_return_hole_symbol" + ]) |> Jason.encode!() {:ok, user_settings} =