mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-08 16:56:03 +00:00
Compare commits
11 Commits
v1.53.1
...
react-flow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea14432d3b | ||
|
|
880b8b6aad | ||
|
|
2a0d7654e7 | ||
|
|
4eb1f641ae | ||
|
|
2da5a243ec | ||
|
|
5ac8ccbe5c | ||
|
|
0568533550 | ||
|
|
178abc2af2 | ||
|
|
adb2a5f459 | ||
|
|
ada1571e1e | ||
|
|
5931c00ff3 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -2,6 +2,31 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.53.4](https://github.com/wanderer-industries/wanderer/compare/v1.53.3...v1.53.4) (2025-03-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* add retry on kills retrieval (#207)
|
||||
|
||||
* add missing masses to wh sizes const (#215)
|
||||
|
||||
## [v1.53.3](https://github.com/wanderer-industries/wanderer/compare/v1.53.2...v1.53.3) (2025-02-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: little bit up performance for windows manager
|
||||
|
||||
## [v1.53.2](https://github.com/wanderer-industries/wanderer/compare/v1.53.1...v1.53.2) (2025-02-27)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.53.1](https://github.com/wanderer-industries/wanderer/compare/v1.53.0...v1.53.1) (2025-02-26)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import clsx from 'clsx';
|
||||
import { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import { ResizableAreaNode } from '@/hooks/Mapper/components/map/components/ResizableAreaNode';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
@@ -59,6 +60,20 @@ const initialNodes: Node<SolarSystemRawType>[] = [
|
||||
// },
|
||||
// type: 'custom',
|
||||
// },
|
||||
{
|
||||
id: '1231213',
|
||||
type: 'resizableAreaNode',
|
||||
width: 200,
|
||||
height: 100,
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
width: 200,
|
||||
height: 100,
|
||||
bgColor: 'rgba(255, 0, 0, 0.2)',
|
||||
// этот коллбэк переопределим позже
|
||||
onResize: () => {},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges = [
|
||||
@@ -128,6 +143,7 @@ const MapComp = ({
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom: nodeComponent,
|
||||
resizableAreaNode: ResizableAreaNode,
|
||||
};
|
||||
}, [nodeComponent]);
|
||||
|
||||
@@ -192,6 +208,9 @@ const MapComp = ({
|
||||
changes[0].selected = getNodes().filter(node => node.selected).length === 1;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('JOipP', `changes`, changes);
|
||||
|
||||
const nextChanges = changes.reduce((acc, change) => {
|
||||
return [...acc, change];
|
||||
}, [] as NodeChange[]);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Rnd } from 'react-rnd';
|
||||
|
||||
export type ResizableAreaData = {
|
||||
width: number;
|
||||
height: number;
|
||||
bgColor: string;
|
||||
onResize: (nodeId: string, newWidth: number, newHeight: number) => void;
|
||||
};
|
||||
|
||||
export const ResizableAreaNode = ({ id, data, selected, dragging, ...rest }: NodeProps<ResizableAreaData>) => {
|
||||
const { width = 200, height = 100, bgColor = 'rgba(255, 0, 0, 0.2)', onResize } = data;
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('JOipP', `ResizableAreaNode`, data);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width,
|
||||
height,
|
||||
border: selected ? '2px solid red' : '2px dashed #999',
|
||||
backgroundColor: bgColor,
|
||||
pointerEvents: dragging ? 'none' : 'auto',
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Rnd
|
||||
enableUserSelectHack={false}
|
||||
disableDragging={true}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
size={{ width, height }}
|
||||
onResizeStop={(e, direction, ref, delta, position) => {
|
||||
const newWidth = parseFloat(ref.style.width);
|
||||
const newHeight = parseFloat(ref.style.height);
|
||||
onResize?.(id, newWidth, newHeight);
|
||||
}}
|
||||
enableResizing={{
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
topRight: true,
|
||||
bottomRight: true,
|
||||
bottomLeft: true,
|
||||
topLeft: true,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', height: '100%', padding: 8 }}>
|
||||
Resizable Area: {width} x {height}
|
||||
</div>
|
||||
</Rnd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ResizableAreaNode';
|
||||
@@ -2,6 +2,7 @@ import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/Sy
|
||||
import { useKillsCounter } from '../../hooks/useKillsCounter';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
@@ -17,17 +18,44 @@ type KillsBookmarkTooltipProps = {
|
||||
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
|
||||
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
|
||||
|
||||
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
|
||||
// Limit the kills shown to match the killsCount parameter
|
||||
const limitedKills = useMemo(() => {
|
||||
if (!detailedKills || detailedKills.length === 0) return [];
|
||||
return detailedKills.slice(0, killsCount);
|
||||
}, [detailedKills, killsCount]);
|
||||
|
||||
if (!killsCount || limitedKills.length === 0 || !systemId || isLoading) return null;
|
||||
|
||||
// Calculate a reasonable height for the tooltip based on the number of kills
|
||||
// but cap it to avoid excessively large tooltips
|
||||
const maxKillsToShow = Math.min(limitedKills.length, 20);
|
||||
const tooltipHeight = Math.max(200, Math.min(500, maxKillsToShow * 35));
|
||||
|
||||
const tooltipContent = (
|
||||
<div style={{ width: '100%', minWidth: '300px', overflow: 'hidden' }}>
|
||||
<SystemKillsContent
|
||||
kills={detailedKills}
|
||||
systemNameMap={systemNameMap}
|
||||
onlyOneSystem={true}
|
||||
autoSize={true}
|
||||
limit={killsCount}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: '400px',
|
||||
height: `${tooltipHeight}px`,
|
||||
maxHeight: '500px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div className="p-2 border-b border-stone-700 bg-stone-800 text-stone-200 font-medium">
|
||||
System Kills ({limitedKills.length})
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<SystemKillsContent
|
||||
kills={limitedKills}
|
||||
systemNameMap={systemNameMap}
|
||||
onlyOneSystem={true}
|
||||
// Don't use autoSize here as we want the virtual scroller to handle scrolling
|
||||
autoSize={false}
|
||||
// We've already limited the kills to match killsCount
|
||||
limit={undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks/useSolarSystemLogic';
|
||||
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
|
||||
@@ -753,8 +753,11 @@ export const SHIP_SIZES_SIZE = {
|
||||
export const SHIP_MASSES_SIZE: Record<number, ShipSizeStatus> = {
|
||||
5_000_000: ShipSizeStatus.small,
|
||||
62_000_000: ShipSizeStatus.medium,
|
||||
300_000_000: ShipSizeStatus.large,
|
||||
375_000_000: ShipSizeStatus.large,
|
||||
1_000_000_000: ShipSizeStatus.freight,
|
||||
1_350_000_000: ShipSizeStatus.capital,
|
||||
1_800_000_000: ShipSizeStatus.capital,
|
||||
2_000_000_000: ShipSizeStatus.capital,
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,11 @@ export const useMapInit = () => {
|
||||
update(updateData);
|
||||
|
||||
if (systems) {
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
// rf.setNod7es(systems.map(convertSystem2Node));
|
||||
const prev = rf.getNodes();
|
||||
const areaNodes = prev.filter(x => x.type !== 'resizableAreaNode');
|
||||
|
||||
rf.setNodes([...areaNodes, ...systems.map(convertSystem2Node)]);
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export * from './useMapHandlers';
|
||||
export * from './useUpdateNodes';
|
||||
export * from './useNodesEdgesState';
|
||||
export * from './useBackgroundVars';
|
||||
export * from './useKillsCounter';
|
||||
export * from './useSystemName';
|
||||
export * from './useNodesEdgesState';
|
||||
export * from './useSolarSystemNode';
|
||||
export * from './useUnsplashedSignatures';
|
||||
export * from './useUpdateNodes';
|
||||
export * from './useNodeKillsCount';
|
||||
|
||||
@@ -27,13 +27,12 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!allKills || allKills.length === 0) return [];
|
||||
|
||||
return [...allKills]
|
||||
.sort((a, b) => {
|
||||
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
})
|
||||
.slice(0, 10);
|
||||
// Sort kills by time, most recent first, but don't limit the number of kills
|
||||
return [...allKills].sort((a, b) => {
|
||||
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
}, [allKills]);
|
||||
|
||||
return {
|
||||
|
||||
31
assets/js/hooks/Mapper/components/map/hooks/useLabelsInfo.ts
Normal file
31
assets/js/hooks/Mapper/components/map/hooks/useLabelsInfo.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
|
||||
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
||||
interface UseLabelsInfoParams {
|
||||
labels: string | null;
|
||||
linkedSigPrefix: string | null;
|
||||
isShowLinkedSigId: boolean;
|
||||
}
|
||||
|
||||
export type LabelInfo = {
|
||||
id: string;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
function sortedLabels(labels: string[]): LabelInfo[] {
|
||||
if (!labels) return [];
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
|
||||
}
|
||||
|
||||
export function useLabelsInfo({ labels, linkedSigPrefix, isShowLinkedSigId }: UseLabelsInfoParams) {
|
||||
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
|
||||
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
|
||||
const labelCustom = useMemo(() => {
|
||||
if (isShowLinkedSigId && linkedSigPrefix) {
|
||||
return labelsManager.customLabel ? `${linkedSigPrefix}・${labelsManager.customLabel}` : linkedSigPrefix;
|
||||
}
|
||||
return labelsManager.customLabel;
|
||||
}, [linkedSigPrefix, isShowLinkedSigId, labelsManager]);
|
||||
|
||||
return { labelsInfo, labelCustom };
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
|
||||
interface Kill {
|
||||
solar_system_id: number | string;
|
||||
kills: number;
|
||||
}
|
||||
|
||||
interface MapEvent {
|
||||
name: Commands;
|
||||
data?: any;
|
||||
payload?: Kill[];
|
||||
}
|
||||
|
||||
export function useNodeKillsCount(
|
||||
systemId: number | string,
|
||||
initialKillsCount: number | null
|
||||
): number | null {
|
||||
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
|
||||
|
||||
useEffect(() => {
|
||||
setKillsCount(initialKillsCount);
|
||||
}, [initialKillsCount]);
|
||||
|
||||
const handleEvent = useCallback((event: MapEvent): boolean => {
|
||||
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
|
||||
const killForSystem = event.payload.find(
|
||||
kill => kill.solar_system_id.toString() === systemId.toString()
|
||||
);
|
||||
if (killForSystem && typeof killForSystem.kills === 'number') {
|
||||
setKillsCount(killForSystem.kills);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [systemId]);
|
||||
|
||||
useMapEventListener(handleEvent);
|
||||
|
||||
return killsCount;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { MapSolarSystemType } from '../map.types';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
@@ -7,19 +7,12 @@ import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
|
||||
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
|
||||
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
|
||||
import { CharacterTypeRaw, Commands, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
|
||||
export type LabelInfo = {
|
||||
id: string;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
|
||||
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
|
||||
import { useSystemName } from './useSystemName';
|
||||
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
|
||||
|
||||
function getActivityType(count: number): string {
|
||||
if (count <= 5) return 'activityNormal';
|
||||
@@ -34,11 +27,6 @@ const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Gallente]: 'Gallente',
|
||||
};
|
||||
|
||||
function sortedLabels(labels: string[]): LabelInfo[] {
|
||||
if (!labels) return [];
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
|
||||
}
|
||||
|
||||
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||
const localCounterCharacters = useMemo(() => {
|
||||
return nodeVars.charactersInSystem
|
||||
@@ -127,21 +115,19 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
|
||||
const linkedSigPrefix = useMemo(() => (linkedSigEveId ? linkedSigEveId.split('-')[0] : null), [linkedSigEveId]);
|
||||
|
||||
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
|
||||
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
|
||||
const labelCustom = useMemo(() => {
|
||||
if (isShowLinkedSigId && linkedSigPrefix) {
|
||||
return labelsManager.customLabel ? `${linkedSigPrefix}・${labelsManager.customLabel}` : linkedSigPrefix;
|
||||
}
|
||||
return labelsManager.customLabel;
|
||||
}, [linkedSigPrefix, isShowLinkedSigId, labelsManager]);
|
||||
const { labelsInfo, labelCustom } = useLabelsInfo({
|
||||
labels,
|
||||
linkedSigPrefix,
|
||||
isShowLinkedSigId,
|
||||
});
|
||||
|
||||
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
|
||||
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
|
||||
|
||||
const hasUserCharacters = useMemo(() => {
|
||||
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
|
||||
}, [charactersInSystem, userCharacters]);
|
||||
const hasUserCharacters = useMemo(
|
||||
() => charactersInSystem.some(x => userCharacters.includes(x.eve_id)),
|
||||
[charactersInSystem, userCharacters],
|
||||
);
|
||||
|
||||
const dbClick = useDoubleClick(() => {
|
||||
outCommand({
|
||||
@@ -153,54 +139,19 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
const showHandlers = isConnecting || hoverNodeId === id;
|
||||
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null;
|
||||
|
||||
const computedTemporaryName = useMemo(() => {
|
||||
if (!isTempSystemNameEnabled) {
|
||||
return '';
|
||||
}
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
}
|
||||
return temporary_name;
|
||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
||||
const { systemName, computedTemporaryName, customName } = useSystemName({
|
||||
isTempSystemNameEnabled,
|
||||
temporary_name,
|
||||
solar_system_name: solar_system_name || '',
|
||||
isShowLinkedSigIdTempName,
|
||||
linkedSigPrefix,
|
||||
name,
|
||||
});
|
||||
|
||||
const systemName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && computedTemporaryName) {
|
||||
return computedTemporaryName;
|
||||
}
|
||||
return solar_system_name;
|
||||
}, [isTempSystemNameEnabled, solar_system_name, computedTemporaryName]);
|
||||
const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures);
|
||||
|
||||
const customName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && computedTemporaryName && name) {
|
||||
return name;
|
||||
}
|
||||
if (solar_system_name !== name && name) {
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
|
||||
|
||||
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
return [[], []];
|
||||
}
|
||||
return prepareUnsplashedChunks(
|
||||
systemSigs
|
||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||
.map(s => ({
|
||||
eve_id: s.eve_id,
|
||||
type: s.type,
|
||||
custom_info: s.custom_info,
|
||||
kind: s.kind,
|
||||
name: s.name,
|
||||
group: s.group,
|
||||
})) as UnsplashedSignatureType[],
|
||||
);
|
||||
}, [isShowUnsplashedSignatures, systemSigs]);
|
||||
|
||||
// Ensure hubs are always strings.
|
||||
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
|
||||
|
||||
const nodeVars: SolarSystemNodeVars = {
|
||||
@@ -225,12 +176,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
dbClick,
|
||||
sortedStatics,
|
||||
effectName: effect_name,
|
||||
regionName: region_name,
|
||||
solarSystemId: solar_system_id.toString(),
|
||||
solarSystemName: solar_system_name,
|
||||
locked,
|
||||
hubs: hubsAsStrings,
|
||||
name: name,
|
||||
name,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
charactersInSystem,
|
||||
@@ -239,6 +188,8 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: computedTemporaryName,
|
||||
regionName: region_name,
|
||||
solarSystemName: solar_system_name,
|
||||
};
|
||||
|
||||
return nodeVars;
|
||||
@@ -281,25 +232,3 @@ export interface SolarSystemNodeVars {
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
}
|
||||
|
||||
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
|
||||
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
|
||||
|
||||
useEffect(() => {
|
||||
setKillsCount(initialKillsCount);
|
||||
}, [initialKillsCount]);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.killsUpdated && event.data?.toString() === systemId.toString()) {
|
||||
//@ts-ignore
|
||||
if (event.payload && typeof event.payload.kills === 'number') {
|
||||
// @ts-ignore
|
||||
setKillsCount(event.payload.kills);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return killsCount;
|
||||
}
|
||||
49
assets/js/hooks/Mapper/components/map/hooks/useSystemName.ts
Normal file
49
assets/js/hooks/Mapper/components/map/hooks/useSystemName.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// useSystemName.ts
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface UseSystemNameParams {
|
||||
isTempSystemNameEnabled: boolean;
|
||||
temporary_name?: string | null;
|
||||
solar_system_name: string;
|
||||
isShowLinkedSigIdTempName: boolean;
|
||||
linkedSigPrefix: string | null;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export function useSystemName({
|
||||
isTempSystemNameEnabled,
|
||||
temporary_name,
|
||||
solar_system_name,
|
||||
isShowLinkedSigIdTempName,
|
||||
linkedSigPrefix,
|
||||
name,
|
||||
}: UseSystemNameParams) {
|
||||
const computedTemporaryName = useMemo(() => {
|
||||
if (!isTempSystemNameEnabled) {
|
||||
return '';
|
||||
}
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
}
|
||||
return temporary_name ?? '';
|
||||
}, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]);
|
||||
|
||||
const systemName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && computedTemporaryName) {
|
||||
return computedTemporaryName;
|
||||
}
|
||||
return solar_system_name;
|
||||
}, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]);
|
||||
|
||||
const customName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && computedTemporaryName && name) {
|
||||
return name;
|
||||
}
|
||||
if (solar_system_name !== name && name) {
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
|
||||
|
||||
return { systemName, computedTemporaryName, customName };
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMemo } from 'react';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||
|
||||
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
|
||||
|
||||
export function useUnsplashedSignatures(systemSigs: SystemSignature[], isShowUnsplashedSignatures: boolean) {
|
||||
return useMemo(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
return {
|
||||
unsplashedLeft: [] as SystemSignature[],
|
||||
unsplashedRight: [] as SystemSignature[],
|
||||
};
|
||||
}
|
||||
const chunks = prepareUnsplashedChunks(
|
||||
systemSigs
|
||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||
.map(s => ({
|
||||
eve_id: s.eve_id,
|
||||
type: s.type,
|
||||
custom_info: s.custom_info,
|
||||
kind: s.kind,
|
||||
name: s.name,
|
||||
group: s.group,
|
||||
})) as UnsplashedSignatureType[],
|
||||
);
|
||||
const [unsplashedLeft, unsplashedRight] = chunks;
|
||||
return { unsplashedLeft, unsplashedRight };
|
||||
}, [isShowUnsplashedSignatures, systemSigs]);
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
const useThrottle = () => {
|
||||
const throttleSeed = useRef<number | null>(null);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const throttleFunction = useRef((func: any, delay = 200) => {
|
||||
if (!throttleSeed.current) {
|
||||
// Call the callback immediately for the first time
|
||||
func();
|
||||
throttleSeed.current = setTimeout(() => {
|
||||
throttleSeed.current = null;
|
||||
@@ -75,7 +75,7 @@ export const useUpdateNodes = (nodes: Node<SolarSystemRawType>[]) => {
|
||||
|
||||
const visibleNodes = new Set(nodes.filter(x => isNodeVisible(x, viewport)).map(x => x.id));
|
||||
update({ visibleNodes });
|
||||
}, [nodes]);
|
||||
}, [getViewport, nodes, update]);
|
||||
|
||||
useOnViewportChange({
|
||||
onChange: () => throttle(updateNodesVisibility.bind(this)),
|
||||
@@ -84,5 +84,5 @@ export const useUpdateNodes = (nodes: Node<SolarSystemRawType>[]) => {
|
||||
|
||||
useEffect(() => {
|
||||
updateNodesVisibility();
|
||||
}, [nodes]);
|
||||
}, [nodes, updateNodesVisibility]);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
|
||||
import { useMapEventListener, MapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
interface UseSystemKillsProps {
|
||||
systemId?: string;
|
||||
@@ -18,11 +19,8 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH
|
||||
const byId: Record<string, DetailedKill> = {};
|
||||
|
||||
for (const kill of [...existing, ...incoming]) {
|
||||
if (!kill.kill_time) {
|
||||
continue;
|
||||
}
|
||||
if (!kill.kill_time) continue;
|
||||
const killTimeMs = new Date(kill.kill_time).valueOf();
|
||||
|
||||
if (killTimeMs >= cutoff) {
|
||||
byId[kill.killmail_id] = kill;
|
||||
}
|
||||
@@ -31,15 +29,42 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH
|
||||
return Object.values(byId);
|
||||
}
|
||||
|
||||
interface DetailedKillsEvent extends MapEvent<Commands> {
|
||||
payload: Record<string, DetailedKill[]>;
|
||||
}
|
||||
|
||||
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
|
||||
const { data, update } = useMapRootState();
|
||||
const { detailedKills = {}, systems = [] } = data;
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const excludedSystems = settings.excludedSystems;
|
||||
|
||||
// When showing all visible kills, filter out excluded systems;
|
||||
// when showAllVisible is false, ignore the exclusion filter.
|
||||
const updateDetailedKills = useCallback(
|
||||
(newKillsMap: Record<string, DetailedKill[]>) => {
|
||||
update(prev => {
|
||||
const oldKills = prev.detailedKills ?? {};
|
||||
const updated = { ...oldKills };
|
||||
for (const [sid, killsArr] of Object.entries(newKillsMap)) {
|
||||
updated[sid] = killsArr;
|
||||
}
|
||||
return { ...prev, detailedKills: updated };
|
||||
}, true);
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
useMapEventListener((event: MapEvent<Commands>) => {
|
||||
if (event.name === Commands.detailedKillsUpdated) {
|
||||
const detailedEvent = event as DetailedKillsEvent;
|
||||
if (systemId && !Object.keys(detailedEvent.payload).includes(systemId.toString())) {
|
||||
return false;
|
||||
}
|
||||
updateDetailedKills(detailedEvent.payload);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const effectiveSystemIds = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
@@ -49,7 +74,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0);
|
||||
|
||||
const mergeKillsIntoGlobal = useCallback(
|
||||
@@ -64,10 +88,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
updated[sid] = combined;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
detailedKills: updated,
|
||||
};
|
||||
return { ...prev, detailedKills: updated };
|
||||
});
|
||||
},
|
||||
[update, sinceHours],
|
||||
@@ -95,7 +116,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
since_hours: sinceHours,
|
||||
};
|
||||
} else {
|
||||
// If there's no system and not showing all, do nothing
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -105,14 +125,11 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
data: requestData,
|
||||
});
|
||||
|
||||
// Single system => `resp.kills`
|
||||
if (resp?.kills) {
|
||||
const arr = resp.kills as DetailedKill[];
|
||||
const sid = systemId ?? 'unknown';
|
||||
mergeKillsIntoGlobal({ [sid]: arr });
|
||||
}
|
||||
// multiple systems => `resp.systems_kills`
|
||||
else if (resp?.systems_kills) {
|
||||
} else if (resp?.systems_kills) {
|
||||
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
|
||||
} else {
|
||||
console.warn('[useSystemKills] Unexpected kills response =>', resp);
|
||||
@@ -142,7 +159,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
} else if (systemId) {
|
||||
return detailedKills[systemId] ?? [];
|
||||
} else if (didFallbackFetch.current) {
|
||||
// if we already did a fallback, we may have data for multiple systems
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
}
|
||||
return [];
|
||||
@@ -153,9 +169,8 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
useEffect(() => {
|
||||
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
|
||||
didFallbackFetch.current = true;
|
||||
// Cancel any queued debounced calls, then do the fallback.
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(true); // forceFallback => fetch as though showAllVisible is true
|
||||
fetchKills(true);
|
||||
}
|
||||
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
|
||||
|
||||
@@ -164,14 +179,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
|
||||
if (showAllVisible || systemId) {
|
||||
debouncedFetchKills();
|
||||
// Clean up the debounce on unmount or changes
|
||||
return () => debouncedFetchKills.cancel();
|
||||
}
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(); // immediate (non-debounced) call
|
||||
fetchKills();
|
||||
}, [debouncedFetchKills, fetchKills]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
||||
import styles from './WindowManager.module.scss';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
import fastDeepEqual from 'fast-deep-equal';
|
||||
|
||||
const MIN_WINDOW_SIZE = 100;
|
||||
const SNAP_THRESHOLD = 10;
|
||||
@@ -100,6 +101,8 @@ export const WindowManager: React.FC<WindowManagerProps> = ({
|
||||
);
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
const ref = useRef({ windows, viewPort, onChange });
|
||||
ref.current = { windows, viewPort, onChange };
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewPort) {
|
||||
@@ -110,6 +113,16 @@ export const WindowManager: React.FC<WindowManagerProps> = ({
|
||||
}, [viewPort]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const next = initialWindows.map(({ content, ...x }) => x);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const prev = ref.current.windows.map(({ content, ...x }) => x);
|
||||
|
||||
// Here we avoid unnecessary renders if changes was emitted from here.
|
||||
if (fastDeepEqual(next, prev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWindows(initialWindows.slice(0));
|
||||
}, [initialWindows]);
|
||||
|
||||
@@ -120,9 +133,6 @@ export const WindowManager: React.FC<WindowManagerProps> = ({
|
||||
const startMousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const startWindowStateRef = useRef<{ x: number; y: number; width: number; height: number }>(DefaultWindowState);
|
||||
|
||||
const ref = useRef({ windows, viewPort, onChange });
|
||||
ref.current = { windows, viewPort, onChange };
|
||||
|
||||
const onDebouncedChange = useMemo(() => {
|
||||
return debounce(() => {
|
||||
ref.current.onChange?.({
|
||||
|
||||
@@ -50,4 +50,8 @@ export default {
|
||||
render(hooks) {
|
||||
this._rootEl.render(<Mapper hooks={hooks} />);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this._rootEl.unmount();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@shopify/draggable": "^1.1.3",
|
||||
"clsx": "^2.1.1",
|
||||
"daisyui": "^4.11.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"live_select": "file:../deps/live_select",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
@@ -31,6 +32,7 @@
|
||||
"react-event-hook": "^3.1.2",
|
||||
"react-flow-renderer": "^10.3.17",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-usestateref": "^1.0.9",
|
||||
"reactflow": "^11.11.4",
|
||||
"tailwindcss": "^3.3.6",
|
||||
|
||||
@@ -1568,6 +1568,11 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
|
||||
@@ -2844,7 +2849,7 @@ lines-and-columns@^1.1.6:
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
"live_select@file:../deps/live_select":
|
||||
version "1.4.2"
|
||||
version "1.5.4"
|
||||
|
||||
locate-path@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -3139,10 +3144,10 @@ path-type@^5.0.0:
|
||||
integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==
|
||||
|
||||
"phoenix@file:../deps/phoenix":
|
||||
version "1.7.14"
|
||||
version "1.7.20"
|
||||
|
||||
"phoenix_html@file:../deps/phoenix_html":
|
||||
version "4.1.0"
|
||||
version "4.2.1"
|
||||
|
||||
"phoenix_live_view@file:../deps/phoenix_live_view":
|
||||
version "0.20.17"
|
||||
@@ -3340,6 +3345,11 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
re-resizable@6.11.2:
|
||||
version "6.11.2"
|
||||
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.11.2.tgz#2e8f7119ca3881d5b5aea0ffa014a80e5c1252b3"
|
||||
integrity sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==
|
||||
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
|
||||
@@ -3356,6 +3366,14 @@ react-dom@^18.3.1:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-draggable@4.4.6:
|
||||
version "4.4.6"
|
||||
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e"
|
||||
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
|
||||
dependencies:
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-error-boundary@^4.0.13:
|
||||
version "4.0.13"
|
||||
resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz"
|
||||
@@ -3402,6 +3420,15 @@ react-refresh@^0.14.2:
|
||||
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz"
|
||||
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
||||
|
||||
react-rnd@^10.5.2:
|
||||
version "10.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.5.2.tgz#47a22c104fb640dae71f149e2c005c879de833bd"
|
||||
integrity sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==
|
||||
dependencies:
|
||||
re-resizable "6.11.2"
|
||||
react-draggable "4.4.6"
|
||||
tslib "2.6.2"
|
||||
|
||||
react-transition-group@^4.4.1:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"
|
||||
@@ -3878,7 +3905,7 @@ ts-interface-checker@^0.1.9:
|
||||
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
|
||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||
|
||||
tslib@^2.6.2:
|
||||
tslib@2.6.2, tslib@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
18
lib/wanderer_app/utils/http_util.ex
Normal file
18
lib/wanderer_app/utils/http_util.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule WandererApp.Utils.HttpUtil do
|
||||
@moduledoc """
|
||||
Utility functions for HTTP operations and error handling.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Determines if an HTTP error is retriable.
|
||||
|
||||
Returns `true` for common transient errors like timeouts and server errors (500, 502, 503, 504).
|
||||
"""
|
||||
def retriable_error?(:timeout), do: true
|
||||
def retriable_error?("Unexpected status: 500"), do: true
|
||||
def retriable_error?("Unexpected status: 502"), do: true
|
||||
def retriable_error?("Unexpected status: 503"), do: true
|
||||
def retriable_error?("Unexpected status: 504"), do: true
|
||||
def retriable_error?("Request failed"), do: true
|
||||
def retriable_error?(_), do: false
|
||||
end
|
||||
@@ -7,6 +7,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
use Retry
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider.{Parser, KillsCache, ZkbApi}
|
||||
alias WandererApp.Utils.HttpUtil
|
||||
|
||||
@page_size 200
|
||||
@max_pages 2
|
||||
@@ -190,9 +191,28 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
defp parse_partial(_other, _cutoff_dt), do: :skip
|
||||
|
||||
defp fetch_full_killmail(k_id, k_hash) do
|
||||
case WandererApp.Esi.get_killmail(k_id, k_hash) do
|
||||
{:ok, full_km} -> {:ok, full_km}
|
||||
{:error, reason} -> {:error, reason}
|
||||
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_killmail(k_id, k_hash) do
|
||||
{:ok, full_km} ->
|
||||
{:ok, full_km}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.warning("[Fetcher] ESI get_killmail timeout => kill_id=#{k_id}, retrying...")
|
||||
raise "ESI timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[Fetcher] ESI get_killmail not_found => kill_id=#{k_id}")
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.warning("[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}")
|
||||
raise "ESI error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||
alias WandererApp.Utils.HttpUtil
|
||||
use Retry
|
||||
|
||||
# Maximum retries for enrichment calls
|
||||
@max_enrichment_retries 2
|
||||
|
||||
@doc """
|
||||
Merges the 'partial' from zKB and the 'full' killmail from ESI, checks its time
|
||||
@@ -254,12 +259,33 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
nil -> km
|
||||
0 -> km
|
||||
eve_id ->
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, %{"name" => char_name}} ->
|
||||
Map.put(km, name_key, char_name)
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, %{"name" => char_name}} ->
|
||||
{:ok, char_name}
|
||||
|
||||
_ ->
|
||||
km
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
|
||||
raise "Character info timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Character info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.debug(fn -> "[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}" end)
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, char_name} -> Map.put(km, name_key, char_name)
|
||||
_ -> km
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -269,18 +295,36 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
nil -> km
|
||||
0 -> km
|
||||
corp_id ->
|
||||
case WandererApp.Esi.get_corporation_info(corp_id) do
|
||||
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_corporation_info(corp_id) do
|
||||
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
|
||||
{:ok, {ticker, corp_name}}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Corporation info timeout, retrying => id=#{corp_id}" end)
|
||||
raise "Corporation info timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Corporation info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, {ticker, corp_name}} ->
|
||||
km
|
||||
|> Map.put(ticker_key, ticker)
|
||||
|> Map.put(name_key, corp_name)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
|
||||
km
|
||||
|
||||
_ ->
|
||||
km
|
||||
_ -> km
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -290,14 +334,36 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
nil -> km
|
||||
0 -> km
|
||||
alliance_id ->
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
|
||||
{:ok, {alliance_ticker, alliance_name}}
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Alliance info timeout, retrying => id=#{alliance_id}" end)
|
||||
raise "Alliance info timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Alliance info error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.debug(fn -> "[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}" end)
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, {alliance_ticker, alliance_name}} ->
|
||||
km
|
||||
|> Map.put(ticker_key, alliance_ticker)
|
||||
|> Map.put(name_key, alliance_name)
|
||||
|
||||
_ ->
|
||||
km
|
||||
_ -> km
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -307,13 +373,31 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
||||
nil -> km
|
||||
0 -> km
|
||||
type_id ->
|
||||
case WandererApp.CachedInfo.get_ship_type(type_id) do
|
||||
{:ok, nil} -> km
|
||||
{:ok, %{name: ship_name}} -> Map.put(km, name_key, ship_name)
|
||||
{:error, reason} ->
|
||||
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
|
||||
km
|
||||
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||
case WandererApp.CachedInfo.get_ship_type(type_id) do
|
||||
{:ok, nil} -> :skip
|
||||
{:ok, %{name: ship_name}} -> {:ok, ship_name}
|
||||
{:error, :timeout} ->
|
||||
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
|
||||
raise "Ship type timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.debug(fn -> "[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}" end)
|
||||
raise "Ship type error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, ship_name} -> Map.put(km, name_key, ship_name)
|
||||
_ -> km
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,8 +7,11 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
require Logger
|
||||
alias WandererApp.Zkb.KillsProvider.Parser
|
||||
alias WandererApp.Esi
|
||||
alias WandererApp.Utils.HttpUtil
|
||||
use Retry
|
||||
|
||||
@heartbeat_interval 1_000
|
||||
@max_esi_retries 3
|
||||
|
||||
# Called by `KillsProvider.handle_connect`
|
||||
def handle_connect(_status, _headers, %{connected: _} = state) do
|
||||
@@ -69,14 +72,39 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
|
||||
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
|
||||
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
|
||||
case Esi.get_killmail(kill_id, kill_hash) do
|
||||
{:ok, full_esi_data} ->
|
||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
|
||||
Parser.parse_and_store_killmail(enriched)
|
||||
|
||||
result = retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
|
||||
case Esi.get_killmail(kill_id, kill_hash) do
|
||||
{:ok, full_esi_data} ->
|
||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
|
||||
Parser.parse_and_store_killmail(enriched)
|
||||
:ok
|
||||
|
||||
{:error, :timeout} ->
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying...")
|
||||
raise "ESI timeout, will retry"
|
||||
|
||||
{:error, :not_found} ->
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}")
|
||||
:skip
|
||||
|
||||
{:error, reason} ->
|
||||
if HttpUtil.retriable_error?(reason) do
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
raise "ESI error: #{inspect(reason)}, will retry"
|
||||
else
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
case result do
|
||||
:ok -> :ok
|
||||
:skip -> :skip
|
||||
{:error, reason} ->
|
||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
Logger.error("[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||
:skip
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user