Compare commits

..

61 Commits

Author SHA1 Message Date
DanSylvest
8bb4998e59 Merge branch 'main' into migrations 2025-10-10 09:44:49 +03:00
DanSylvest
c825a3f4c4 fix(Map): Fixed problem with a lot unnecessary loads zkb data on resize map 2025-10-10 09:34:25 +03:00
CI
5343c34488 chore: [skip ci] 2025-10-09 19:57:07 +00:00
CI
4878be1a53 chore: release version v1.81.5 2025-10-09 19:57:07 +00:00
Dmitry Popov
1ff689c26c fix(Core): Update connection ship size based on linked signature type 2025-10-09 21:56:23 +02:00
CI
79b660e899 chore: [skip ci] 2025-10-09 18:45:16 +00:00
CI
665a679bd5 chore: release version v1.81.4 2025-10-09 18:45:16 +00:00
Dmitry Popov
7bd634eb95 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-10-09 20:44:42 +02:00
Dmitry Popov
c3b5a77a86 fix(Core): Fixed signature to system link issues 2025-10-09 20:44:33 +02:00
DanSylvest
8498846d9c fix(Map): Added ability to see focused element 2025-10-09 15:52:48 +03:00
CI
12f39a0133 chore: [skip ci] 2025-10-07 20:59:47 +00:00
CI
ffc2a86e95 chore: release version v1.81.3 2025-10-07 20:59:47 +00:00
Dmitry Popov
82babf41a2 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-10-07 22:59:10 +02:00
Dmitry Popov
81055b4fbd fix(Core): Fixed cancel ping errors 2025-10-07 22:59:01 +02:00
CI
5070a59f88 chore: [skip ci] 2025-10-07 20:49:34 +00:00
CI
65d5bf960d chore: release version v1.81.2 2025-10-07 20:49:34 +00:00
Dmitry Popov
8fc4cb190e Merge pull request #526 from guarzo/guarzo/apicustom
fix: api dropping custom name
2025-10-08 00:49:00 +04:00
DanSylvest
c6c065dbb9 fix(Map): Removed unnecessary vertical scroller in Character Tracking dialog. Main always first in list of tracking characters, following next after main, another characters sorting by name 2025-10-07 15:06:29 +03:00
DanSylvest
8ba34533d7 fix(Map): Added Search tool for systems what on the map 2025-10-07 14:07:13 +03:00
Guarzo
095a4b2362 fix: api dropping custom name 2025-10-06 15:52:35 -04:00
CI
fafc631e49 chore: [skip ci] 2025-10-02 21:37:58 +00:00
CI
e56383c8b1 chore: release version v1.81.1 2025-10-02 21:37:58 +00:00
Dmitry Popov
b9c26bdb04 fix(Core): Fixed characters tracking updates. 2025-10-02 23:37:23 +02:00
CI
8aeaa81752 chore: [skip ci] 2025-10-02 16:09:27 +00:00
CI
b16ec0490f chore: release version v1.81.0 2025-10-02 16:09:27 +00:00
Dmitry Popov
eceaf1d73b Merge pull request #523 from dedo1911/feat/pwa
feat(core): fix pwa icons + add screen in manifest
2025-10-02 20:08:57 +04:00
dedo1911
34cf668a33 feat(core): fix pwa icons + add screen in manifest 2025-10-02 17:57:52 +02:00
CI
c22d410c9f chore: [skip ci] 2025-10-02 11:39:21 +00:00
CI
fc6af867f2 chore: release version v1.80.0 2025-10-02 11:39:21 +00:00
Dmitry Popov
2d96114984 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-10-02 13:38:50 +02:00
Dmitry Popov
fd7e19e490 feat(Core): Added PWA web manifest 2025-10-02 13:38:46 +02:00
CI
f7d996f5b2 chore: [skip ci] 2025-10-01 14:32:20 +00:00
CI
f8ab1383ab chore: release version v1.79.6 2025-10-01 14:32:20 +00:00
Dmitry Popov
e1559aac94 fix(Core): Fixed modals auto-save on Enter. 2025-10-01 16:31:48 +02:00
CI
2e17cce5cd chore: [skip ci] 2025-10-01 13:57:58 +00:00
CI
8fb831f171 chore: release version v1.79.5 2025-10-01 13:57:58 +00:00
Dmitry Popov
cb84f34515 fix(Core): Fixed system details modal auto-save on Enter. 2025-10-01 15:57:26 +02:00
CI
272cce1a77 chore: [skip ci] 2025-09-30 13:00:53 +00:00
CI
e0e3ed1580 chore: release version v1.79.4 2025-09-30 13:00:53 +00:00
Dmitry Popov
c4c848cf37 fix(Core): Fixed updating connection time status based on linked signature data. Fixed FR gas sites parsing.
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-09-30 15:00:14 +02:00
DanSylvest
d33a2e3a5b Merge branch 'refs/heads/main' into migrations
# Conflicts:
#	assets/js/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ServerSettings.tsx
2025-09-28 21:23:28 +03:00
DanSylvest
5c8753fb96 fix(Map): Added migration mechanism 2025-09-28 09:29:42 +03:00
CI
32d25d86eb chore: [skip ci] 2025-09-27 15:37:16 +00:00
CI
863adccac1 chore: release version v1.79.3 2025-09-27 15:37:16 +00:00
Dmitry Popov
2d527e1d16 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-27 17:36:40 +02:00
Dmitry Popov
9a64ad6fa7 fix(Core): Fixed connection passages count 2025-09-27 17:36:35 +02:00
CI
5ce472ebff chore: [skip ci] 2025-09-26 18:28:30 +00:00
CI
76588af12f chore: release version v1.79.2 2025-09-26 18:28:30 +00:00
Dmitry Popov
134f169eb9 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-26 20:28:01 +02:00
Dmitry Popov
7c2d731c4c chore: fix 2025-09-26 20:27:54 +02:00
CI
c7e2a290cf chore: [skip ci] 2025-09-26 18:27:38 +00:00
CI
5ea966892a chore: release version v1.79.1 2025-09-26 18:27:38 +00:00
Dmitry Popov
b879db76b7 chore: fix 2025-09-26 20:27:06 +02:00
CI
d13a628029 chore: [skip ci] 2025-09-26 18:18:51 +00:00
CI
7c1e2595e3 chore: release version v1.79.0 2025-09-26 18:18:51 +00:00
Dmitry Popov
a99e8a915e Merge pull request #522 from wanderer-industries/update-lifetime
Update lifetime
2025-09-26 22:18:20 +04:00
DanSylvest
f89cd5f44f fix(Map): Remove settings some default values if migration from very old settings system 2025-09-25 21:21:24 +03:00
DanSylvest
abe4951251 Merge branch 'main' into migrations 2025-09-25 21:20:49 +03:00
DanSylvest
5508fbee2f fix(Map): MIGRATION: support from old store settings import 2025-09-24 11:07:52 +03:00
DanSylvest
51ff4e7f36 fix(Map): Add common migration mechanism. ATTENTION! This is a non-reversible stored map settings commit — it means we do not guarantee that settings will work if you check out back. We’ve tried to migrate old settings, but it may not work well or may NOT work at all. 2025-09-24 11:03:17 +03:00
DanSylvest
34d3d92afd fix(Map): Add front-end migrations for local store settings 2025-09-22 12:24:34 +03:00
97 changed files with 1971 additions and 676 deletions

View File

@@ -13,4 +13,4 @@ export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
export WANDERER_SSE_ENABLED="true"
export WANDERER_WEBHOOKS_ENABLED="true"
export WANDERER_SSE_MAX_CONNECTIONS="1000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"

View File

@@ -2,6 +2,132 @@
<!-- changelog -->
## [v1.81.5](https://github.com/wanderer-industries/wanderer/compare/v1.81.4...v1.81.5) (2025-10-09)
### Bug Fixes:
* Core: Update connection ship size based on linked signature type
## [v1.81.4](https://github.com/wanderer-industries/wanderer/compare/v1.81.3...v1.81.4) (2025-10-09)
### Bug Fixes:
* Core: Fixed signature to system link issues
## [v1.81.3](https://github.com/wanderer-industries/wanderer/compare/v1.81.2...v1.81.3) (2025-10-07)
### Bug Fixes:
* Core: Fixed cancel ping errors
## [v1.81.2](https://github.com/wanderer-industries/wanderer/compare/v1.81.1...v1.81.2) (2025-10-07)
### Bug Fixes:
* api dropping custom name
## [v1.81.1](https://github.com/wanderer-industries/wanderer/compare/v1.81.0...v1.81.1) (2025-10-02)
### Bug Fixes:
* Core: Fixed characters tracking updates.
## [v1.81.0](https://github.com/wanderer-industries/wanderer/compare/v1.80.0...v1.81.0) (2025-10-02)
### Features:
* core: fix pwa icons + add screen in manifest
## [v1.80.0](https://github.com/wanderer-industries/wanderer/compare/v1.79.6...v1.80.0) (2025-10-02)
### Features:
* Core: Added PWA web manifest
## [v1.79.6](https://github.com/wanderer-industries/wanderer/compare/v1.79.5...v1.79.6) (2025-10-01)
### Bug Fixes:
* Core: Fixed modals auto-save on Enter.
## [v1.79.5](https://github.com/wanderer-industries/wanderer/compare/v1.79.4...v1.79.5) (2025-10-01)
### Bug Fixes:
* Core: Fixed system details modal auto-save on Enter.
## [v1.79.4](https://github.com/wanderer-industries/wanderer/compare/v1.79.3...v1.79.4) (2025-09-30)
### Bug Fixes:
* Core: Fixed updating connection time status based on linked signature data. Fixed FR gas sites parsing.
## [v1.79.3](https://github.com/wanderer-industries/wanderer/compare/v1.79.2...v1.79.3) (2025-09-27)
### Bug Fixes:
* Core: Fixed connection passages count
## [v1.79.2](https://github.com/wanderer-industries/wanderer/compare/v1.79.1...v1.79.2) (2025-09-26)
## [v1.79.1](https://github.com/wanderer-industries/wanderer/compare/v1.79.0...v1.79.1) (2025-09-26)
## [v1.79.0](https://github.com/wanderer-industries/wanderer/compare/v1.78.1...v1.79.0) (2025-09-26)
### Features:
* Core: Updated connections EOL logic
### Bug Fixes:
* Map: Fixed eslint problems
* Map: Update lifetime design and buttons
* Map: Update wormhole lifetime UI and removed unnecessary code
## [v1.78.1](https://github.com/wanderer-industries/wanderer/compare/v1.78.0...v1.78.1) (2025-09-24)

View File

@@ -284,3 +284,7 @@
border-left-color: #e67e22;
}
.p-dialog-header-icon.p-dialog-header-close.p-link {
position: relative;
left: 6px;
}

View File

@@ -1,7 +1,7 @@
.vertical-tabs-container {
display: flex;
width: 100%;
min-height: 400px;
min-height: 200px;
.p-tabview {
width: 100%;

View File

@@ -1,4 +1,4 @@
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const REQUIRED_KEYS = [
'widgets',
@@ -19,11 +19,8 @@ export class MapUserSettingsParseError extends Error {
}
}
const isNumber = (v: unknown): v is number => typeof v === 'number' && !Number.isNaN(v);
/** Minimal check that an object matches SettingsWithVersion<*> */
const isSettingsWithVersion = (v: unknown): v is SettingsWithVersion<unknown> =>
typeof v === 'object' && v !== null && isNumber((v as any).version) && 'settings' in (v as any);
/** Minimal check that an object matches SettingsWrapper<*> */
const isSettings = (v: unknown): v is SettingsWrapper<unknown> => typeof v === 'object' && v !== null;
/** Ensure every required key is present */
const hasAllRequiredKeys = (v: unknown): v is Record<RequiredKeys, unknown> =>
@@ -52,8 +49,8 @@ export const parseMapUserSettings = (json: unknown): MapUserSettings => {
}
for (const key of REQUIRED_KEYS) {
if (!isSettingsWithVersion((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWithVersion<T>`);
if (!isSettings((data as any)[key])) {
throw new MapUserSettingsParseError(`"${key}" must match SettingsWrapper<T>`);
}
}

View File

@@ -0,0 +1,26 @@
import { useMemo } from 'react';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
export type UseLocalCounterProps = {
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
};
export const getLocalCharacters = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
return charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
};
export const useLocalCounter = ({ charactersInSystem, userCharacters }: UseLocalCounterProps) => {
const localCounterCharacters = useMemo(
() => getLocalCharacters({ charactersInSystem, userCharacters }),
[charactersInSystem, userCharacters],
);
return { localCounterCharacters };
};

View File

@@ -1,11 +1,10 @@
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import type { PanelPosition } from '@reactflow/core';
import clsx from 'clsx';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
import ReactFlow, {
Background,
Edge,
@@ -33,19 +32,9 @@ import {
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
import { useBackgroundVars } from './hooks/useBackgroundVars';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
const getViewPortFromStore = () => {
const restored = localStorage.getItem(SESSION_KEY.viewPort);
if (!restored) {
return { ...DEFAULT_VIEW_PORT };
}
return JSON.parse(restored);
};
import { MapViewport, OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import type { Viewport } from '@reactflow/core/dist/esm/types';
import { usePrevious } from 'primereact/hooks';
const initialNodes: Node<SolarSystemRawType>[] = [
// {
@@ -88,6 +77,7 @@ interface MapCompProps {
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
onChangeViewport?: (viewport: MapViewport) => void;
minimapClasses?: string;
isShowMinimap?: boolean;
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
@@ -99,6 +89,7 @@ interface MapCompProps {
pings: PingData[];
minimapPlacement?: PanelPosition;
localShowShipName?: boolean;
defaultViewport?: Viewport;
}
const MapComp = ({
@@ -119,19 +110,25 @@ const MapComp = ({
pings,
minimapPlacement = 'bottom-right',
localShowShipName = false,
onChangeViewport,
defaultViewport,
}: MapCompProps) => {
const { getNodes } = useReactFlow();
const { getNodes, setViewport } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const { variant, gap, size, color } = useBackgroundVars(theme);
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
const refVars = useRef({ onChangeViewport });
refVars.current = { onChangeViewport };
const nodeTypes = useMemo(() => {
return {
custom: nodeComponent,
@@ -187,9 +184,10 @@ const MapComp = ({
[onSelectionChange],
);
const handleMoveEnd: OnMoveEnd = (_, viewport) => {
localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport));
};
const handleMoveEnd: OnMoveEnd = useCallback((_, viewport) => {
// @ts-ignore
refVars.current.onChangeViewport?.(viewport);
}, []);
const handleNodesChange = useCallback(
(changes: NodeChange[]) => {
@@ -218,6 +216,19 @@ const MapComp = ({
}));
}, [showKSpaceBG, isThickConnections, pings, update, localShowShipName]);
const prevViewport = usePrevious(defaultViewport);
useEffect(() => {
if (defaultViewport == null) {
return;
}
if (prevViewport == null) {
return;
}
setViewport(defaultViewport);
}, [defaultViewport, prevViewport, setViewport]);
return (
<>
<div
@@ -232,7 +243,7 @@ const MapComp = ({
onConnect={onConnect}
// TODO we need save into session all of this
// and on any action do either
defaultViewport={getViewPortFromStore()}
defaultViewport={defaultViewport}
edgeTypes={edgeTypes}
nodeTypes={nodeTypes}
connectionMode={connectionMode}

View File

@@ -11,6 +11,7 @@ export type MapData = MapUnionTypes & {
isThickConnections: boolean;
linkedSigEveId: string;
localShowShipName: boolean;
systemHighlighted: string | undefined;
};
interface MapProviderProps {
@@ -44,6 +45,7 @@ const INITIAL_DATA: MapData = {
userHubs: [],
pings: [],
localShowShipName: false,
systemHighlighted: undefined,
};
export interface MapContextProps {

View File

@@ -5,8 +5,8 @@
}
.hoverTarget {
padding: 0.5rem;
margin: -0.5rem;
padding: 2px;
margin: -2px;
display: inline-block;
}

View File

@@ -13,9 +13,19 @@ interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;
hasUserCharacters: boolean;
showIcon?: boolean;
disableInteractive?: boolean;
className?: string;
contentClassName?: string;
}
export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIcon = true }: LocalCounterProps) => {
export const LocalCounter = ({
className,
contentClassName,
localCounterCharacters,
hasUserCharacters,
showIcon = true,
disableInteractive,
}: LocalCounterProps) => {
const {
data: { localShowShipName },
} = useMapState();
@@ -42,22 +52,30 @@ export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIc
return (
<div
className={clsx(classes.TooltipActive, {
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
})}
className={clsx(
classes.TooltipActive,
{
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
},
className,
)}
>
<WdTooltipWrapper
content={pilotTooltipContent}
position={TooltipPosition.right}
offset={0}
interactive={true}
interactive={!disableInteractive}
smallPaddings
>
<div className={clsx(classes.hoverTarget)}>
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: hasUserCharacters,
})}
className={clsx(
classes.localCounter,
{
[classes.hasUserCharacters]: hasUserCharacters,
},
contentClassName,
)}
>
{showIcon && <i className="pi pi-users" />}
<span>{localCounterCharacters.length}</span>

View File

@@ -1,14 +1,6 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
@keyframes move-stripes {
from {

View File

@@ -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, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -17,10 +17,12 @@ import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-
import { Tag } from 'primereact/tag';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
@@ -139,12 +141,26 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center gap-1 justify-end">
<div className={clsx('flex items-center gap-1')}>
<div className="flex items-center gap-0.5 justify-end">
<div className={clsx('flex items-center gap-0.5')}>
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
)}
{nodeVars.description != null && nodeVars.description !== '' && (
<WdTooltipWrapper
className="h-[15px] transform -translate-y-[6%]"
position={TooltipPosition.top}
content={`System have description`}
>
<i
className={clsx(
'pi hero-chat-bubble-bottom-center-text w-[10px] h-[10px]',
'text-[8px] relative top-[1px]',
)}
/>
</WdTooltipWrapper>
)}
</div>
<LocalCounter
@@ -177,6 +193,17 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</>
)}
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
<div
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
>
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
</div>
)}
<div className={classes.Handlers}>
<Handle
type="source"

View File

@@ -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';
import { useNodeKillsCount, useSolarSystemNode } from '../../hooks';
import {
EFFECT_BACKGROUND_STYLES,
MARKER_BOOKMARK_BG_STYLES,
@@ -16,6 +16,7 @@ import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
import { useLocalCounter } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
@@ -172,6 +173,17 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
</>
)}
{nodeVars.systemHighlighted === nodeVars.solarSystemId && (
<div
className={clsx('absolute top-[-4px] left-[-4px]', 'w-[calc(100%+8px)] h-[calc(100%+8px)]', 'animate-pulse')}
>
<div className="absolute left-0 top-0 w-3 h-2 border-t-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 top-0 w-3 h-2 border-t-2 border-r-2 border-sky-300"></div>
<div className="absolute left-0 bottom-0 w-3 h-2 border-b-2 border-l-2 border-sky-300"></div>
<div className="absolute right-0 bottom-0 w-3 h-2 border-b-2 border-r-2 border-sky-300"></div>
</div>
)}
<div className={classes.Handlers}>
<Handle
type="source"

View File

@@ -1,12 +1,18 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandCenterSystem } from '@/hooks/Mapper/types';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SYSTEM_FOCUSED_LIFETIME } from '@/hooks/Mapper/constants.ts';
export const useCenterSystem = () => {
const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
const { update } = useMapState();
const ref = useRef({ rf, update });
ref.current = { rf, update };
const highlightTimeout = useRef<number>();
return useCallback((systemId: CommandCenterSystem) => {
const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId);
@@ -14,5 +20,16 @@ export const useCenterSystem = () => {
return;
}
ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 });
ref.current.update({ systemHighlighted: systemId });
if (highlightTimeout.current !== undefined) {
clearTimeout(highlightTimeout.current);
}
highlightTimeout.current = setTimeout(() => {
highlightTimeout.current = undefined;
ref.current.update({ systemHighlighted: undefined });
}, SYSTEM_FOCUSED_LIFETIME);
}, []);
};

View File

@@ -5,7 +5,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { Regions, REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { Regions, REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
@@ -50,27 +50,9 @@ export interface SolarSystemNodeVars {
isRally: boolean;
classTitle: string | null;
temporaryName?: string | null;
}
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
const localCounterCharacters = useMemo(() => {
return nodeVars.charactersInSystem
.map(char => ({
...char,
compact: true,
isOwn: nodeVars.userCharacters.includes(char.eve_id),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
return { localCounterCharacters };
description: string | null;
comments_count: number | null;
systemHighlighted: string | undefined;
}
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
@@ -84,6 +66,8 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
labels,
temporary_name,
linked_sig_eve_id: linkedSigEveId = '',
description,
comments_count,
} = data;
const {
@@ -125,6 +109,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
showKSpaceBG,
isThickConnections,
pings,
systemHighlighted,
},
outCommand,
} = useMapState();
@@ -169,7 +154,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] || null : null;
const regionClass = showKSpaceBG ? SPACE_TO_CLASS[space] || null : null;
const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled,
@@ -232,6 +217,9 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
regionName,
solarSystemName: solar_system_name,
isRally,
description,
comments_count,
systemHighlighted,
};
return nodeVars;

View File

@@ -10,3 +10,5 @@ export type OnMapSelectionChange = (event: {
}) => void;
export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void;
export type MapViewport = { zoom: 1; x: 0; y: 0 };

View File

@@ -0,0 +1,8 @@
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);

View File

@@ -1,15 +1,15 @@
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { IconField } from 'primereact/iconfield';
import { AutoComplete } from 'primereact/autocomplete';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WdButton, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { AutoComplete } from 'primereact/autocomplete';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { useCallback, useRef, useState } from 'react';
import classes from './AddSystemDialog.module.scss';
import clsx from 'clsx';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
@@ -115,90 +115,93 @@ export const AddSystemDialog = ({
setVisible(false);
}}
>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
<form onSubmit={handleSubmit}>
<div className="flex flex-col gap-3 px-1.5">
<div className="flex flex-col gap-2 py-3.5">
<div className="flex flex-col gap-1">
<IconField>
<AutoComplete
ref={inputRef}
multiple
showEmptyMessage
scrollHeight="300px"
value={selectedItem}
suggestions={filteredItems}
completeMethod={searchItems}
onChange={e => {
setSelectedItem(e.value.length < 2 ? e.value : [e.value[e.value.length - 1]]);
}}
emptyMessage="Not found any system..."
placeholder="Type here..."
field="label"
id="value"
className="w-full"
itemTemplate={(item: SearchSystemItem) => {
const { security, system_class, effect_power, effect_name, statics } = item.system_static_info;
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
return (
<div className={clsx('flex gap-1.5', classes.SearchItem)}>
<SystemViewStandalone
security={security}
system_class={system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
{isWH && (
<div className="flex gap-1 grow justify-between">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
</div>
</div>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
)}
</div>
);
}}
selectedItemTemplate={(item: SearchSystemItem) => (
<SystemViewStandalone
security={item.system_static_info.security}
system_class={item.system_static_info.system_class}
solar_system_id={item.value}
class_title={item.class_title}
solar_system_name={item.label}
region_name={item.region_name}
/>
)}
/>
</IconField>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
<span className="text-[12px] text-stone-400 ml-1">*to search type at least 2 symbols.</span>
</div>
</div>
<div className="flex gap-2 justify-end">
<WdButton
type="submit"
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
<div className="flex gap-2 justify-end">
<WdButton
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
</form>
</Dialog>
);
};

View File

@@ -1,12 +1,12 @@
import { InputText } from 'primereact/inputtext';
import { Dialog } from 'primereact/dialog';
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { useCallback, useEffect, useRef, useState } from 'react';
interface SystemCustomLabelDialog {
systemId: string;
@@ -125,7 +125,7 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
</div>
<div className="flex gap-2 justify-end">
<WdButton onClick={handleSave} outlined size="small" label="Save"></WdButton>
<WdButton type="submit" onClick={handleSave} outlined size="small" label="Save"></WdButton>
</div>
</div>
</form>

View File

@@ -1,11 +1,11 @@
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { SystemView, WdButton } from '@/hooks/Mapper/components/ui-kit';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemView, WdButton } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
import { Dialog } from 'primereact/dialog';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useRef, useState } from 'react';
const PING_TITLES = {
[PingType.Rally]: 'RALLY',
@@ -62,7 +62,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
</div>
}
visible={visible}
draggable={false}
draggable={true}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
@@ -91,7 +91,7 @@ export const SystemPingDialog = ({ systemId, type, visible, setVisible }: System
</div>
<div className="flex gap-2 justify-end">
<WdButton onClick={handleSave} size="small" severity="danger" label="Ping!" />
<WdButton type="submit" onClick={handleSave} size="small" severity="danger" label="Ping!" />
</div>
</div>
</form>

View File

@@ -1,15 +1,15 @@
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield';
import { TooltipPosition, WdButton, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { OutCommand } from '@/hooks/Mapper/types';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useEffect, useRef, useState } from 'react';
interface SystemSettingsDialog {
systemId: string;
@@ -125,7 +125,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
}}
>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3 px-2">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<label htmlFor="username">Custom name</label>
@@ -225,7 +225,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
</div>
<div className="flex gap-2 justify-end">
<WdButton onClick={handleSave} outlined size="small" label="Save" />
<WdButton onClick={handleSave} outlined size="small" label="Save" type="submit" />
</div>
</div>
</form>

View File

@@ -1,9 +1,9 @@
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useRef, useState } from 'react';
interface RoutesSettingsDialog {
visible: boolean;

View File

@@ -1,8 +1,8 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import debounce from 'lodash.debounce';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { DetailedKill } from '@/hooks/Mapper/types/kills';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useStableValue } from '@/hooks/Mapper/hooks';
interface UseSystemKillsProps {
systemId?: string;
@@ -30,9 +30,8 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
update,
storedSettings: { settingsKills },
} = useMapRootState();
const { excludedSystems } = settingsKills;
const effectiveSinceHours = sinceHours;
const excludedSystems = useStableValue(settingsKills.excludedSystems);
const effectiveSystemIds = useMemo(() => {
if (showAllVisible) {
@@ -76,13 +75,13 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
eventType = OutCommand.getSystemsKills;
requestData = {
system_ids: effectiveSystemIds,
since_hours: effectiveSinceHours,
since_hours: sinceHours,
};
} else if (systemId) {
eventType = OutCommand.getSystemKills;
requestData = {
system_id: systemId,
since_hours: effectiveSinceHours,
since_hours: sinceHours,
};
} else {
setIsLoading(false);
@@ -110,16 +109,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
setIsLoading(false);
}
},
[showAllVisible, systemId, outCommand, effectiveSystemIds, effectiveSinceHours, mergeKillsIntoGlobal],
);
const debouncedFetchKills = useMemo(
() =>
debounce(fetchKills, 500, {
leading: true,
trailing: false,
}),
[fetchKills],
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
);
const finalKills = useMemo(() => {
@@ -141,27 +131,22 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
useEffect(() => {
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
didFallbackFetch.current = true;
debouncedFetchKills.cancel();
fetchKills(true);
}
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
}, [systemId, showAllVisible, fetchKills]);
useEffect(() => {
if (effectiveSystemIds.length === 0) return;
if (showAllVisible || systemId) {
// Cancel any pending debounced fetch
debouncedFetchKills.cancel();
// Fetch kills immediately
fetchKills();
return () => debouncedFetchKills.cancel();
return;
}
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills, fetchKills]);
}, [showAllVisible, systemId, effectiveSystemIds, fetchKills]);
const refetch = useCallback(() => {
debouncedFetchKills.cancel();
fetchKills();
}, [debouncedFetchKills, fetchKills]);
}, [fetchKills]);
return {
kills: finalKills,

View File

@@ -15,6 +15,7 @@ import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
import { OldSettingsDialog } from '@/hooks/Mapper/components/mapRootContent/components/OldSettingsDialog.tsx';
import { TopSearch } from '@/hooks/Mapper/components/mapRootContent/components/TopSearch';
export interface MapRootContentProps {}
@@ -72,6 +73,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar>
<div className="flex items-center ml-1">
<TopSearch />
<PingsInterface />
<MapContextMenu
onShowOnTheMap={handleShowOnTheMap}

View File

@@ -62,7 +62,7 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
header="Map user settings"
visible
draggable={false}
style={{ width: '600px' }}
className="w-[600px] h-[400px]"
onShow={handleShow}
onHide={handleHide}
>

View File

@@ -5,6 +5,8 @@ import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { saveTextFile } from '@/hooks/Mapper/utils/saveToFile.ts';
import { SplitButton } from 'primereact/splitbutton';
import { loadTextFile } from '@/hooks/Mapper/utils';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
export const ImportExport = () => {
const {
@@ -22,8 +24,9 @@ export const ImportExport = () => {
}
try {
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
if (applySettings(applyMigrations(parsed) || createDefaultStoredSettings())) {
toast.current?.show({
severity: 'success',
summary: 'Import',
@@ -59,8 +62,9 @@ export const ImportExport = () => {
try {
const text = await loadTextFile();
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
const parsed = parseMapUserSettings(text);
if (applySettings(parsed)) {
if (applySettings(applyMigrations(parsed))) {
toast.current?.show({
severity: 'success',
summary: 'Import',

View File

@@ -1,13 +1,13 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { OutCommand } from '@/hooks/Mapper/types';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
import { callToastSuccess } from '@/hooks/Mapper/helpers';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
export const ServerSettings = () => {
@@ -29,15 +29,16 @@ export const ServerSettings = () => {
}
if (res?.default_settings == null) {
applySettings(createDefaultWidgetSettings());
applySettings(createDefaultStoredSettings());
return;
}
try {
applySettings(parseMapUserSettings(res.default_settings));
//INFO: INSTEAD CHECK WE WILL TRY TO APPLY MIGRATION
applySettings(applyMigrations(JSON.parse(res.default_settings)));
callToastSuccess(toast.current, 'Settings synchronized successfully');
} catch (error) {
applySettings(createDefaultWidgetSettings());
applySettings(createDefaultStoredSettings());
}
}, [applySettings, outCommand]);

View File

@@ -1,6 +1,6 @@
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
import { InterfaceStoredSettingsProps } from '@/hooks/Mapper/mapRootProvider';
import { AvailableThemes, MiniMapPlacement, PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
export const DEFAULT_REMOTE_SETTINGS = {
[UserSettingsRemoteProps.link_signature_on_splash]: false,
@@ -51,7 +51,7 @@ export const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
export const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
label: 'Delete connections to linked signatures',
label: 'Delete connections with linked signatures',
type: 'checkbox',
},
{

View File

@@ -1,14 +1,5 @@
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { saveTextFile } from '@/hooks/Mapper/utils';
import { ConfirmPopup } from 'primereact/confirmpopup';
@@ -18,10 +9,7 @@ import { useCallback, useRef } from 'react';
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return {
version: -1,
settings: lsSettings ? JSON.parse(lsSettings) : defaultValues,
};
return lsSettings ? JSON.parse(lsSettings) : defaultValues;
};
export const OldSettingsDialog = () => {
@@ -44,13 +32,15 @@ export const OldSettingsDialog = () => {
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_6');
const out: MapUserSettings = {
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createSettings(widgetLocal, DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createSettings(widgetsOld, getDefaultWidgetProps()),
routes: createSettings(widgetRoutes, DEFAULT_ROUTES_SETTINGS),
onTheMap: createSettings(onTheMapOld, DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createSettings(signatures, DEFAULT_SIGNATURE_SETTINGS),
interface: createSettings(interfaceSettings, STORED_INTERFACE_DEFAULT_VALUES),
version: 0,
migratedFromOld: false,
killsWidget: createSettings(widgetKills, {}),
localWidget: createSettings(widgetLocal, {}),
widgets: createSettings(widgetsOld, {}),
routes: createSettings(widgetRoutes, {}),
onTheMap: createSettings(onTheMapOld, {}),
signaturesWidget: createSettings(signatures, {}),
interface: createSettings(interfaceSettings, {}),
};
if (asFile) {

View File

@@ -7,6 +7,7 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { TopSearch } from '@/hooks/Mapper/components/mapRootContent/components/TopSearch';
// import { DebugComponent } from '@/hooks/Mapper/components/mapRootContent/components/RightBar/DebugComponent.tsx';
interface RightBarProps {
@@ -48,7 +49,7 @@ export const RightBar = ({
classes.RightBarRoot,
'w-full h-full',
'text-gray-200 shadow-lg border-l border-zinc-800 border-opacity-70 bg-opacity-70 bg-neutral-900',
'flex flex-col items-center justify-between',
'flex flex-col items-center justify-between pt-1',
)}
>
<div className="flex flex-col gap-2 items-center mt-1">
@@ -65,15 +66,31 @@ export const RightBar = ({
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowOnTheMap}
>
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
<div className="flex flex-col gap-1">
<TopSearch
customBtn={open => (
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={open}
>
<i className="pi pi-search"></i>
</button>
</WdTooltipWrapper>
)}
/>
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowOnTheMap}
>
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
</div>
</>
)}
{additionalContent}

View File

@@ -0,0 +1,170 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
:root {
--rf-has-user-characters: #ffc75d;
}
.Sidebar {
:global {
.p-sidebar-header {
padding-left: 14px;
padding-right: 14px;
}
.p-sidebar-content {
padding-left: 0px;
padding-right: 0px;
}
}
}
.Content {
position: relative;
overflow: hidden;
&.Pochven,
&.Mataria,
&.Amarria,
&.Gallente,
&.Caldaria {
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: 50% 50%;
z-index: -1;
background-repeat: no-repeat;
border-radius: 3px;
}
}
&.Mataria {
&::after {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
//background-position-x: 52px;
//background-position-y: -83px;
background-position-x: 331px;
background-position-y: -77px;
transform: scale(-1);
}
}
&.Caldaria {
&::after {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
//background-position-x: 41px;
//background-position-y: -45px;
background-position-x: 360px;
background-position-y: -16px;
background-size: 30%;
transform: rotate(180deg);
}
}
&.Amarria {
&::after {
opacity: 0.45;
background-image: url('/images/amarr-small.png');
//background-position-x: 23px;
//background-position-y: -74px;
background-position-x: -1px;
background-position-y: -75px;
background-origin: border-box;
background-size: auto;
transform: scale(1, -1);
}
}
&.Gallente {
&::after {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
//background-position-x: 1px;
//background-position-y: -55px;
background-position-x: 294px;
background-position-y: -53px;
transform: scale(-1);
}
}
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -88px;
}
}
&.selected {
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
&.rally {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
$neon-color-3 0px,
$neon-color-3 8px,
transparent 8px,
transparent 21px
);
background-size: 30px 30px;
animation: move-stripes 3s linear infinite;
}
}
&.eve-system-status-home {
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);
&.selected {
border-color: var(--eve-solar-system-status-color-home);
}
}
&.eve-system-status-friendly {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-friendly-dark30), transparent);
&.selected {
border-color: var(--eve-solar-system-status-color-friendly-dark5);
}
}
&.eve-system-status-lookingFor {
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: $pastel-pink;
}
}
&.eve-system-status-warning {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-warning), transparent);
}
&.eve-system-status-dangerous {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-dangerous), transparent);
}
&.eve-system-status-target {
background-image: linear-gradient(275deg, var(--eve-solar-system-status-target), transparent);
}
}

View File

@@ -0,0 +1,347 @@
import classes from './TopSearch.module.scss';
import { Sidebar } from 'primereact/sidebar';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import { Commands, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import {
SystemViewStandalone,
TooltipPosition,
WdImageSize,
WdImgButton,
WdTooltipWrapper,
WHClassView,
WHEffectView,
} from '@/hooks/Mapper/components/ui-kit';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { STATUS_CLASSES } from '@/hooks/Mapper/components/map/constants.ts';
import { REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants.ts';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { getLocalCharacters } from '@/hooks/Mapper/components/hooks/useLocalCounter.ts';
import { PrimeIcons } from 'primereact/api';
type CompiledSystem = {
dynamic: SolarSystemRawType;
static: SolarSystemStaticInfoRaw | undefined;
};
const useItemTemplate = () => {
const {
data: { wormholesData, characters, userCharacters, hubs },
} = useMapRootState();
return useCallback(
(item: CompiledSystem, options: VirtualScrollerTemplateOptions) => {
if (!item.static) {
return null;
}
const {
security,
system_class,
class_title,
effect_power,
region_name,
solar_system_name,
solar_system_id,
effect_name,
statics,
region_id,
} = item.static;
const onlineCharactersInSystem = characters.filter(
c => c.location?.solar_system_id === solar_system_id && c.online,
);
const hasOnlineUserCharacters = onlineCharactersInSystem.some(x => userCharacters.includes(x.eve_id));
const onlineCharacters = getLocalCharacters({ charactersInSystem: onlineCharactersInSystem, userCharacters });
const offlineCharactersInSystem = characters.filter(
c => c.location?.solar_system_id === solar_system_id && !c.online,
);
const hasOfflineUserCharacters = offlineCharactersInSystem.some(x => userCharacters.includes(x.eve_id));
const offlineCharacters = getLocalCharacters({ charactersInSystem: offlineCharactersInSystem, userCharacters });
const handleSelect = () => {
emitMapEvent({
name: Commands.centerSystem,
data: solar_system_id.toString(),
});
};
const sortedStatics = sortWHClasses(wormholesData, statics);
const isWH = isWormholeSpace(system_class);
const regionClass = SPACE_TO_CLASS[REGIONS_MAP[region_id]] || null;
const showTempName = item.dynamic.temporary_name != null;
const showCustomName = item.dynamic.name != null && item.dynamic.name !== solar_system_name;
return (
<div
className={clsx(
'w-full box-border px-3.5 py-1 h-[48px] cursor-pointer',
'bg-transparent hover:bg-stone-800/30 transition-all !duration-250 ease-in-out',
{
'surface-hover': options.odd,
['border-b border-gray-600 border-opacity-20']: !options.last,
},
classes.Content,
regionClass && classes[regionClass],
item.dynamic.status !== undefined && classes[STATUS_CLASSES[item.dynamic.status]],
)}
onClick={handleSelect}
>
<div className={clsx('w-full')}>
<div className={clsx('grid grid-cols-[1fr_auto] gap-1.5 w-full')}>
<div className="flex [&>*]:!text-[13px] gap-1.5">
<SystemViewStandalone
className="!text-[13px]"
security={security}
system_class={system_class}
solar_system_id={parseInt(item.dynamic.id)}
class_title={class_title}
solar_system_name={`${solar_system_name}`}
region_name={region_name}
nameClassName="font-semibold"
/>
{(showTempName || showCustomName) && (
<div className="grid grid-cols-[auto_1fr] gap-1.5 text-stone-400 text-[12px]">
{showTempName && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{item.dynamic.temporary_name}
</span>
)}
{showCustomName && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap">{item.dynamic.name}</span>
)}
</div>
)}
{effect_name && isWH && (
<WHEffectView
effectName={effect_name}
effectPower={effect_power}
className={classes.SearchItemEffect}
/>
)}
</div>
<div>
{isWH && (
<div className="flex gap-1 grow justify-between !text-[13px]">
<div></div>
<div className="flex gap-1">
{sortedStatics.map(x => (
<WHClassView key={x} whClassName={x} />
))}
</div>
</div>
)}
</div>
</div>
<div className="flex gap-1.5 text-[13px] pl-[2px] items-center h-[20px]">
<LocalCounter
disableInteractive
className="[&_span]:!text-[12px] [&_i]:!text-[13px]"
hasUserCharacters={hasOnlineUserCharacters}
localCounterCharacters={onlineCharacters}
/>
<LocalCounter
disableInteractive
className="[&_span]:!text-[12px] [&_i]:!text-[13px] text-stone-[400]"
contentClassName={clsx('!text-stone-500', { ['!text-yellow-600']: hasOfflineUserCharacters })}
hasUserCharacters={hasOfflineUserCharacters}
localCounterCharacters={offlineCharacters}
/>
{item.dynamic.locked && <i className={clsx(PrimeIcons.LOCK, 'text-[11px] relative top-[1px]')} />}
{hubs.includes(solar_system_id.toString()) && (
<i className={clsx(PrimeIcons.MAP_MARKER, 'text-[11px] relative top-[1px]')} />
)}
{item.dynamic.comments_count != null && item.dynamic.comments_count > 0 && (
<WdTooltipWrapper
position={TooltipPosition.top}
content={`[${item.dynamic.comments_count}] Comments in System - click to system to see it in Comments Widget`}
>
<i className={clsx(PrimeIcons.COMMENT, 'text-[11px] relative top-[1px]')} />
</WdTooltipWrapper>
)}
{item.dynamic.description != null && item.dynamic.description !== '' && (
<WdTooltipWrapper
position={TooltipPosition.top}
content={`System have description - click to system to see it in Info Widget`}
>
<i
className={clsx(
'pi hero-chat-bubble-bottom-center-text w-[14px] h-[14px]',
'text-[8px] relative top-[-1px]',
)}
/>
</WdTooltipWrapper>
)}
{/*kek*/}
</div>
</div>
</div>
);
},
[characters, hubs, userCharacters, wormholesData],
);
};
export interface TopSearchSidebarProps {
show: boolean;
onHide: () => void;
}
export const TopSearchSidebar = ({ show, onHide }: TopSearchSidebarProps) => {
const [searchVal, setSearchVal] = useState('');
// eslint-disable-next-line
const inputRef = useRef<any>();
const {
data: { systems },
} = useMapRootState();
const itemTemplate = useItemTemplate();
const systemsCompiled = useMemo<CompiledSystem[]>(() => {
return systems.map(x => ({
dynamic: x,
static: getSystemStaticInfo(x.id),
}));
}, [systems]);
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
const filtered = useMemo(() => {
let out = systemsCompiled;
out = out.sort((a, b) => {
// 1. Status > 0 — always on top and ASC
if (a.dynamic.status > 0 && b.dynamic.status > 0) return a.dynamic.status - b.dynamic.status;
if (a.dynamic.status > 0) return -1;
if (b.dynamic.status > 0) return 1;
// 2. IF status = 0, J priority
const aStartsWithJ = a.dynamic.name?.startsWith('J') ?? false;
const bStartsWithJ = b.dynamic.name?.startsWith('J') ?? false;
if (aStartsWithJ && !bStartsWithJ) return -1;
if (!aStartsWithJ && bStartsWithJ) return 1;
// 3. IF both starts with J or not - sort by name
const nameA = a.dynamic.name ?? '';
const nameB = b.dynamic.name ?? '';
return nameA.localeCompare(nameB);
});
const normalized = searchVal.toLowerCase();
if (searchVal !== '') {
out = out.filter(x => {
if (x.static?.solar_system_name.toLowerCase().includes(normalized)) {
return true;
}
if (x.dynamic.name?.toLowerCase().includes(normalized)) {
return true;
}
if (x.dynamic.temporary_name?.toLowerCase().includes(normalized)) {
return true;
}
return false;
});
}
return out;
}, [searchVal, systemsCompiled]);
return (
<Sidebar
className={clsx(classes.Sidebar, 'bg-neutral-900 !p-[0px] w-[500px]')}
visible={show}
position="right"
onShow={onShow}
onHide={onHide}
modal={false}
header={`Search [${filtered.length}]`}
icons={<></>}
>
<div className={clsx('grid grid-rows-[auto_1fr] gap-y-[8px] h-full')}>
<div className={'flex justify-between items-center gap-2 px-2 pt-1'}>
<IconField className="w-full">
{searchVal.length > 0 && (
<WdImgButton
className="pi pi-trash"
textSize={WdImageSize.large}
tooltip={{
content: 'Clear',
className: 'pi p-input-icon',
position: TooltipPosition.top,
}}
onClick={() => setSearchVal('')}
/>
)}
<InputText
id="label"
className="w-full"
aria-describedby="label"
ref={inputRef}
autoComplete="off"
value={searchVal}
placeholder="Type To Search"
onChange={e => setSearchVal(e.target.value)}
/>
</IconField>
</div>
<VirtualScroller
items={filtered}
itemSize={48}
itemTemplate={itemTemplate}
className={clsx(
classes.VirtualScroller,
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
'[&>div]:w-full',
)}
autoSize={false}
/>
</div>
</Sidebar>
);
};
interface TopSearchProps {
customBtn?: (open: () => void) => React.ReactNode;
}
export const TopSearch = ({ customBtn }: TopSearchProps) => {
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
return (
<>
{customBtn != null && customBtn(() => setOpenAddSystem(true))}
{customBtn == null && (
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent px-2 relative left-1"
type="button"
onClick={() => setOpenAddSystem(true)}
>
<i className="pi pi-search text-lg"></i>
</button>
)}
<TopSearchSidebar show={openAddSystem} onHide={() => setOpenAddSystem(false)} />
</>
);
};

View File

@@ -0,0 +1 @@
export * from './TopSearch.tsx';

View File

@@ -1,13 +1,13 @@
import { Column } from 'primereact/column';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { DataTable } from 'primereact/datatable';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TrackingCharacter } from '@/hooks/Mapper/types';
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
export const TrackingCharactersList = () => {
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
const { trackingCharacters, updateTracking } = useTracking();
const { trackingCharacters, main, following, updateTracking } = useTracking();
const refVars = useRef({ trackingCharacters });
refVars.current = { trackingCharacters };
@@ -20,9 +20,31 @@ export const TrackingCharactersList = () => {
[updateTracking],
);
const items = useMemo(() => {
let out = trackingCharacters;
out = out.sort((a, b) => {
const aId = a.character.eve_id;
const bId = b.character.eve_id;
// 1. main always first
if (aId === main && bId !== main) return -1;
if (bId === main && aId !== main) return 1;
// 2. following after main
if (aId === following && bId !== following) return -1;
if (bId === following && aId !== following) return 1;
// 3. sort by name
return a.character.name.localeCompare(b.character.name);
});
return out;
}, [trackingCharacters, main, following]);
return (
<DataTable
value={trackingCharacters}
value={items}
size="small"
selectionMode={null}
selection={selected}

View File

@@ -14,7 +14,7 @@ import { Connections } from '@/hooks/Mapper/components/mapRootContent/components
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Node, useReactFlow, XYPosition } from 'reactflow';
import { Node, useReactFlow, Viewport, XYPosition } from 'reactflow';
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
@@ -48,7 +48,7 @@ export const MapWrapper = () => {
linkSignatureToSystem,
systemSignatures,
},
storedSettings: { interfaceSettings, settingsLocal },
storedSettings: { interfaceSettings, settingsLocal, mapSettings, mapSettingsUpdate },
} = useMapRootState();
const {
@@ -83,8 +83,17 @@ export const MapWrapper = () => {
systems,
systemSignatures,
deleteSystems,
mapSettingsUpdate,
});
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, systemSignatures, deleteSystems };
ref.current = {
selectedConnections,
selectedSystems,
systemContextProps,
systems,
systemSignatures,
deleteSystems,
mapSettingsUpdate,
};
useMapEventListener(event => {
runCommand(event);
@@ -121,6 +130,10 @@ export const MapWrapper = () => {
[update],
);
const handleChangeViewport = useCallback((viewport: Viewport) => {
ref.current.mapSettingsUpdate({ viewport });
}, []);
const handleCommand: OutCommandHandler = useCallback(
event => {
switch (event.type) {
@@ -259,6 +272,7 @@ export const MapWrapper = () => {
onConnectionInfoClick={handleConnectionDbClick}
onSystemContextMenu={handleSystemContextMenu}
onSelectionContextMenu={handleSystemMultipleContext}
onChangeViewport={handleChangeViewport}
minimapClasses={minimapClasses}
isShowMinimap={showMinimap}
showKSpaceBG={isShowKSpace}
@@ -270,6 +284,7 @@ export const MapWrapper = () => {
onAddSystem={onAddSystem}
minimapPlacement={minimapPosition}
localShowShipName={settingsLocal.showShipName}
defaultViewport={mapSettings.viewport}
/>
{openSettings != null && (

View File

@@ -14,6 +14,7 @@ export type SystemViewStandaloneStatic = Pick<
export type SystemViewStandaloneProps = {
hideRegion?: boolean;
customName?: string;
nameClassName?: string;
compact?: boolean;
onContextMenu?(e: MouseEvent, systemId: string): void;
} & WithClassName &
@@ -23,6 +24,7 @@ export type SystemViewStandaloneProps = {
export const SystemViewStandalone = ({
className,
nameClassName,
hideRegion,
customName,
class_title,
@@ -57,10 +59,14 @@ export const SystemViewStandalone = ({
>
<span className={clsx(classTitleColor)}>{class_title}</span>
<span
className={clsx('text-gray-200 whitespace-nowrap', {
['overflow-hidden text-ellipsis']: compact,
[classes.CompactName]: compact,
})}
className={clsx(
'text-gray-200 whitespace-nowrap',
{
['overflow-hidden text-ellipsis']: compact,
[classes.CompactName]: compact,
},
nameClassName,
)}
>
{customName ?? solar_system_name}
</span>

View File

@@ -1,10 +1,10 @@
import classes from './WHClassView.module.scss';
import { Tooltip } from 'primereact/tooltip';
import clsx from 'clsx';
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit/InfoDrawer';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import { useMemo } from 'react';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
const prepareMass = (mass: number) => {
if (mass === 0) {
@@ -45,47 +45,40 @@ export const WHClassView = ({
const whClass = useMemo(() => WORMHOLES_ADDITIONAL_INFO[whData.dest], [whData.dest]);
const whClassStyle = WORMHOLE_CLASS_STYLES[whClass?.wormholeClassID] ?? '';
const uid = useMemo(() => new Date().getTime().toString(), []);
return (
<div className={clsx(classes.WHClassViewRoot, className)}>
{!hideTooltip && (
<Tooltip
target={`.wh-name${whClassName}${uid}`}
position="right"
mouseTrack
mouseTrackLeft={20}
mouseTrackTop={30}
className="border border-green-300 rounded border-opacity-10 bg-stone-900 bg-opacity-90 "
<WdTooltipWrapper
position={TooltipPosition.bottom}
content={
<div className="flex gap-3">
<div className="flex flex-col gap-1">
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
</div>
<div className="flex flex-col gap-1">
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
</div>
</div>
}
>
<div className="flex gap-3">
<div className="flex flex-col gap-1">
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
</div>
<div className="flex flex-col gap-1">
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
</div>
<div
className={clsx(
classes.WHClassViewContent,
{ [classes.NoOffset]: noOffset },
'wh-name select-none cursor-help',
)}
>
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
{!hideWhClass && whClass && (
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
{useShortTitle ? whClass.shortTitle : whClass.shortName}
</span>
)}
</div>
</Tooltip>
</WdTooltipWrapper>
)}
<div
className={clsx(
classes.WHClassViewContent,
{ [classes.NoOffset]: noOffset },
'wh-name select-none cursor-help',
`wh-name${whClassName}${uid}`,
)}
>
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
{!hideWhClass && whClass && (
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
{useShortTitle ? whClass.shortTitle : whClass.shortName}
</span>
)}
</div>
</div>
);
};

View File

@@ -7,6 +7,8 @@ export enum SESSION_KEY {
routes = 'routes',
}
export const SYSTEM_FOCUSED_LIFETIME = 10000;
export const GRADIENT_MENU_ACTIVE_CLASSES = 'bg-gradient-to-br from-transparent/10 to-fuchsia-300/10';
export enum Regions {
@@ -151,3 +153,11 @@ export const MINIMAP_PLACEMENT_MAP = {
[PingsPlacement.rightBottom]: 'bottom-right',
[PingsPlacement.leftBottom]: 'bottom-left',
};
export const SPACE_TO_CLASS: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};

View File

@@ -5,3 +5,4 @@ export * from './useHotkey';
export * from './usePageVisibility';
export * from './useSkipContextMenu';
export * from './useThrottle';
export * from './useStableValue';

View File

@@ -0,0 +1,8 @@
import { useRef } from 'react';
import fastDeepEuqal from 'fast-deep-equal';
export const useStableValue = <T>(value: T): T => {
const ref = useRef(value);
if (!fastDeepEuqal(ref.current, value)) ref.current = value;
return ref.current;
};

View File

@@ -23,12 +23,14 @@ import {
InterfaceStoredSettings,
KillsWidgetSettings,
LocalWidgetSettings,
MapSettings,
MapUserSettings,
OnTheMapSettingsType,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_MAP_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
@@ -127,6 +129,8 @@ export interface MapRootContextProps {
settingsOnTheMapUpdate: Dispatch<SetStateAction<OnTheMapSettingsType>>;
settingsKills: KillsWidgetSettings;
settingsKillsUpdate: Dispatch<SetStateAction<KillsWidgetSettings>>;
mapSettings: MapSettings;
mapSettingsUpdate: Dispatch<SetStateAction<MapSettings>>;
isReady: boolean;
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
@@ -172,6 +176,8 @@ const MapRootContext = createContext<MapRootContextProps>({
settingsOnTheMapUpdate: () => null,
settingsKills: DEFAULT_KILLS_WIDGET_SETTINGS,
settingsKillsUpdate: () => null,
mapSettings: DEFAULT_MAP_SETTINGS,
mapSettingsUpdate: () => null,
isReady: false,
hasOldSettings: false,
getSettingsForExport: () => '',

View File

@@ -3,16 +3,13 @@ import {
InterfaceStoredSettings,
KillsWidgetSettings,
LocalWidgetSettings,
MapSettings,
MiniMapPlacement,
OnTheMapSettingsType,
PingsPlacement,
RoutesType,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
CURRENT_WINDOWS_VERSION,
DEFAULT_WIDGETS,
STORED_VISIBLE_WIDGETS_DEFAULT,
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
import { DEFAULT_WIDGETS, STORED_VISIBLE_WIDGETS_DEFAULT } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMenu: false,
@@ -43,25 +40,25 @@ export const DEFAULT_ROUTES_SETTINGS: RoutesType = {
export const DEFAULT_WIDGET_LOCAL_SETTINGS: LocalWidgetSettings = {
compact: true,
showOffline: false,
version: 0,
showShipName: false,
};
export const DEFAULT_ON_THE_MAP_SETTINGS: OnTheMapSettingsType = {
hideOffline: false,
version: 0,
};
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
showAll: false,
whOnly: true,
excludedSystems: [],
version: 2,
timeRange: 4,
};
export const DEFAULT_MAP_SETTINGS: MapSettings = {
viewport: { zoom: 1, x: 0, y: 0 },
};
export const getDefaultWidgetProps = () => ({
version: CURRENT_WINDOWS_VERSION,
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
windows: DEFAULT_WIDGETS,
});

View File

@@ -0,0 +1,55 @@
import { MapUserSettings, SettingsTypes, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_MAP_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { STORED_SETTINGS_VERSION } from '@/hooks/Mapper/mapRootProvider/version.ts';
// TODO - we need provide and compare version
export const createWidgetSettings = <T>(settings: T) => {
return settings;
};
export const createDefaultStoredSettings = (): MapUserSettings => {
return {
version: STORED_SETTINGS_VERSION,
migratedFromOld: true,
killsWidget: createWidgetSettings(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettings(getDefaultWidgetProps()),
routes: createWidgetSettings(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES),
map: createWidgetSettings(DEFAULT_MAP_SETTINGS),
};
};
// INFO - in another case need to generate complex type - but looks like it unnecessary
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const getDefaultSettingsByType = (type: SettingsTypes): SettingsWrapper<any> => {
switch (type) {
case SettingsTypes.killsWidget:
return createWidgetSettings(DEFAULT_KILLS_WIDGET_SETTINGS);
case SettingsTypes.localWidget:
return createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS);
case SettingsTypes.widgets:
return createWidgetSettings(getDefaultWidgetProps());
case SettingsTypes.routes:
return createWidgetSettings(DEFAULT_ROUTES_SETTINGS);
case SettingsTypes.onTheMap:
return createWidgetSettings(DEFAULT_ON_THE_MAP_SETTINGS);
case SettingsTypes.signaturesWidget:
return createWidgetSettings(DEFAULT_SIGNATURE_SETTINGS);
case SettingsTypes.interface:
return createWidgetSettings(STORED_INTERFACE_DEFAULT_VALUES);
case SettingsTypes.map:
return createWidgetSettings(DEFAULT_MAP_SETTINGS);
}
};

View File

@@ -1,30 +0,0 @@
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
// TODO - we need provide and compare version
const createWidgetSettingsWithVersion = <T>(settings: T) => {
return {
version: 0,
settings,
};
};
export const createDefaultWidgetSettings = (): MapUserSettings => {
return {
killsWidget: createWidgetSettingsWithVersion(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettingsWithVersion(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettingsWithVersion(getDefaultWidgetProps()),
routes: createWidgetSettingsWithVersion(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettingsWithVersion(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettingsWithVersion(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettingsWithVersion(STORED_INTERFACE_DEFAULT_VALUES),
};
};

View File

@@ -5,8 +5,8 @@ import {
MapUserSettingsStructure,
RemoteAdminSettingsResponse,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
import { applyMigrations } from '@/hooks/Mapper/mapRootProvider/migrations';
interface UseActualizeRemoteMapSettingsProps {
outCommand: OutCommandHandler;
@@ -37,14 +37,14 @@ export const useActualizeRemoteMapSettings = ({
}
if (res?.default_settings == null) {
applySettings(createDefaultWidgetSettings());
applySettings(createDefaultStoredSettings());
return;
}
try {
applySettings(parseMapUserSettings(res.default_settings));
applySettings(applyMigrations(JSON.parse(res.default_settings)));
} catch (error) {
applySettings(createDefaultWidgetSettings());
applySettings(createDefaultStoredSettings());
}
}, [outCommand]);

View File

@@ -63,123 +63,127 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const { pingAdded, pingCancelled } = useCommandPings();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.updateActivity:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
emitMapEvent({ name: type, data });
},
};
}, []);
emitMapEvent({ name: type, data });
},
};
},
[],
);
};

View File

@@ -6,7 +6,9 @@ import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/
import fastDeepEqual from 'fast-deep-equal';
import { OutCommandHandler } from '@/hooks/Mapper/types';
import { useActualizeRemoteMapSettings } from '@/hooks/Mapper/mapRootProvider/hooks/useActualizeRemoteMapSettings.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
import { applyMigrations, extractData } from '@/hooks/Mapper/mapRootProvider/migrations';
import { LS_KEY, LS_KEY_LEGASY } from '@/hooks/Mapper/mapRootProvider/version.ts';
const EMPTY_OBJ = {};
@@ -14,7 +16,7 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
const [isReady, setIsReady] = useState(false);
const [hasOldSettings, setHasOldSettings] = useState(false);
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>('map-user-settings', {
const [mapUserSettings, setMapUserSettings] = useLocalStorageState<MapUserSettingsStructure>(LS_KEY, {
defaultValue: EMPTY_OBJ,
});
@@ -83,13 +85,20 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
'killsWidget',
);
const [windowsSettings, setWindowsSettings] = useSettingsValueAndSetter(
const [windowsSettings, windowsSettingsUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'widgets',
);
const [mapSettings, mapSettingsUpdate] = useSettingsValueAndSetter(
mapUserSettings,
setMapUserSettings,
map_slug,
'map',
);
// HERE we MUST work with migrations
useEffect(() => {
if (isReady) {
@@ -100,36 +109,28 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
return;
}
if (mapUserSettings[map_slug] == null) {
const currentMapUserSettings = mapUserSettings[map_slug];
if (currentMapUserSettings == null) {
return;
}
// TODO !!!! FROM this date 06.07.2025 - we must work only with migrations
// actualizeSettings(STORED_INTERFACE_DEFAULT_VALUES, interfaceSettings, setInterfaceSettings);
// actualizeSettings(DEFAULT_ROUTES_SETTINGS, settingsRoutes, settingsRoutesUpdate);
// actualizeSettings(DEFAULT_WIDGET_LOCAL_SETTINGS, settingsLocal, settingsLocalUpdate);
// actualizeSettings(DEFAULT_SIGNATURE_SETTINGS, settingsSignatures, settingsSignaturesUpdate);
// actualizeSettings(DEFAULT_ON_THE_MAP_SETTINGS, settingsOnTheMap, settingsOnTheMapUpdate);
// actualizeSettings(DEFAULT_KILLS_WIDGET_SETTINGS, settingsKills, settingsKillsUpdate);
try {
// INFO: after migrations migratedFromOld always will be true
const migratedResult = applyMigrations(
!currentMapUserSettings.migratedFromOld ? extractData(LS_KEY_LEGASY) : currentMapUserSettings,
);
setIsReady(true);
}, [
map_slug,
mapUserSettings,
interfaceSettings,
setInterfaceSettings,
settingsRoutes,
settingsRoutesUpdate,
settingsLocal,
settingsLocalUpdate,
settingsSignatures,
settingsSignaturesUpdate,
settingsOnTheMap,
settingsOnTheMapUpdate,
settingsKills,
settingsKillsUpdate,
isReady,
]);
if (!migratedResult) {
setIsReady(true);
return;
}
setMapUserSettings({ ...mapUserSettings, [map_slug]: migratedResult });
setIsReady(true);
} catch (error) {
setIsReady(true);
}
}, [isReady, mapUserSettings, map_slug, setMapUserSettings]);
const checkOldSettings = useCallback(() => {
const interfaceSettings = localStorage.getItem('window:interface:settings');
@@ -157,7 +158,7 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
}, []);
const resetSettings = useCallback(() => {
applySettings(createDefaultWidgetSettings());
applySettings(createDefaultStoredSettings());
}, [applySettings]);
return {
@@ -177,7 +178,9 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
settingsKills,
settingsKillsUpdate,
windowsSettings,
setWindowsSettings,
windowsSettingsUpdate,
mapSettings,
mapSettingsUpdate,
getSettingsForExport,
applySettings,

View File

@@ -1,15 +1,11 @@
import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from 'react';
import {
MapUserSettings,
MapUserSettingsStructure,
SettingsWithVersion,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MapUserSettings, MapUserSettingsStructure, SettingsWrapper } from '@/hooks/Mapper/mapRootProvider/types.ts';
type ExtractSettings<S extends keyof MapUserSettings> =
MapUserSettings[S] extends SettingsWithVersion<infer U> ? U : never;
type ExtractSettings<S extends keyof MapUserSettings> = MapUserSettings[S] extends SettingsWrapper<infer U> ? U : never;
type Setter<S extends keyof MapUserSettings> = (
value: Partial<ExtractSettings<S>> | ((prev: ExtractSettings<S>) => Partial<ExtractSettings<S>>),
version?: number,
) => void;
type GenerateSettingsReturn<S extends keyof MapUserSettings> = [ExtractSettings<S>, Setter<S>];
@@ -24,7 +20,7 @@ export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
if (!mapId) return {} as ExtractSettings<S>;
const mapSettings = settings[mapId];
return (mapSettings?.[setting]?.settings ?? ({} as ExtractSettings<S>)) as ExtractSettings<S>;
return (mapSettings?.[setting] ?? ({} as ExtractSettings<S>)) as ExtractSettings<S>;
}, [mapId, setting, settings]);
const refData = useRef({ mapId, setting, setSettings });
@@ -37,8 +33,7 @@ export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
setSettings(all => {
const currentMap = all[mapId];
const prev = currentMap[setting].settings as ExtractSettings<S>;
const version = currentMap[setting].version;
const prev = currentMap[setting] as ExtractSettings<S>;
const patch =
typeof value === 'function' ? (value as (p: ExtractSettings<S>) => Partial<ExtractSettings<S>>)(prev) : value;
@@ -47,10 +42,7 @@ export const useSettingsValueAndSetter = <S extends keyof MapUserSettings>(
...all,
[mapId]: {
...currentMap,
[setting]: {
version,
settings: { ...(prev as any), ...patch } as ExtractSettings<S>,
},
[setting]: { ...(prev as any), ...patch } as ExtractSettings<S>,
},
};
});

View File

@@ -6,7 +6,6 @@ import { getDefaultWidgetProps } from '@/hooks/Mapper/mapRootProvider/constants.
export type StoredWindowProps = Omit<WindowProps, 'content'>;
export type WindowStoreInfo = {
version: number;
windows: StoredWindowProps[];
visible: WidgetsIds[];
viewPort?: { w: number; h: number } | undefined;
@@ -16,19 +15,18 @@ export type ToggleWidgetVisibility = (widgetId: WidgetsIds) => void;
interface UseStoreWidgetsProps {
windowsSettings: WindowStoreInfo;
setWindowsSettings: Dispatch<SetStateAction<WindowStoreInfo>>;
windowsSettingsUpdate: Dispatch<SetStateAction<WindowStoreInfo>>;
}
export const useStoreWidgets = ({ windowsSettings, setWindowsSettings }: UseStoreWidgetsProps) => {
const ref = useRef({ windowsSettings, setWindowsSettings });
ref.current = { windowsSettings, setWindowsSettings };
export const useStoreWidgets = ({ windowsSettings, windowsSettingsUpdate }: UseStoreWidgetsProps) => {
const ref = useRef({ windowsSettings, windowsSettingsUpdate });
ref.current = { windowsSettings, windowsSettingsUpdate };
const updateWidgetSettings: WindowsManagerOnChange = useCallback(({ windows, viewPort }) => {
const { setWindowsSettings } = ref.current;
const { windowsSettingsUpdate } = ref.current;
setWindowsSettings(({ version, visible /*, windows*/ }: WindowStoreInfo) => {
windowsSettingsUpdate(({ visible /*, windows*/ }: WindowStoreInfo) => {
return {
version,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
windows: DEFAULT_WIDGETS.map(({ content, ...x }) => {
const windowProp = windows.find(j => j.id === x.id);
@@ -45,9 +43,9 @@ export const useStoreWidgets = ({ windowsSettings, setWindowsSettings }: UseStor
}, []);
const toggleWidgetVisibility: ToggleWidgetVisibility = useCallback(widgetId => {
const { setWindowsSettings } = ref.current;
const { windowsSettingsUpdate } = ref.current;
setWindowsSettings(({ visible, windows, ...x }) => {
windowsSettingsUpdate(({ visible, windows, ...x }) => {
const isCheckedPrev = visible.includes(widgetId);
if (!isCheckedPrev) {
const maxZIndex = Math.max(...windows.map(w => w.zIndex));
@@ -72,7 +70,7 @@ export const useStoreWidgets = ({ windowsSettings, setWindowsSettings }: UseStor
});
}, []);
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
const resetWidgets = useCallback(() => ref.current.windowsSettingsUpdate(getDefaultWidgetProps()), []);
return {
windowsSettings,

View File

@@ -0,0 +1,58 @@
import { MapUserSettingsStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { STORED_SETTINGS_VERSION } from '@/hooks/Mapper/mapRootProvider/version.ts';
import { migrations } from '@/hooks/Mapper/mapRootProvider/migrations/index.ts';
import { createDefaultStoredSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultStoredSettings.ts';
export const extractData = (localStoreKey = 'map-user-settings'): MapUserSettingsStructure | null => {
const val = localStorage.getItem(localStoreKey);
if (!val) {
return null;
}
return JSON.parse(val);
};
export const applyMigrations = (mapSettings: any) => {
let currentMapSettings = { ...mapSettings };
// INFO if we have NO any data in store expected that we will use default
if (!currentMapSettings) {
return;
}
const direction = STORED_SETTINGS_VERSION - (currentMapSettings.version || 0);
if (direction === 0) {
if (currentMapSettings.version == null) {
return { ...currentMapSettings, version: STORED_SETTINGS_VERSION, migratedFromOld: true };
}
return;
}
const cmVersion = currentMapSettings.version || 0;
// downgrade
// INFO: when we downgrading - if diff between >= 1 it means was major version
if (direction < 0) {
// If was minor version - we do nothing
if (Math.abs(direction) < 1) {
return currentMapSettings;
}
// if was major version - we set default settings
return createDefaultStoredSettings();
}
const preparedMigrations = migrations
.sort((a, b) => a.to - b.to)
.filter(x => x.to > cmVersion && x.to <= STORED_SETTINGS_VERSION);
for (const migration of preparedMigrations) {
const { to, up } = migration;
const next = up(currentMapSettings);
currentMapSettings = { ...next, version: to, migratedFromOld: true };
}
return currentMapSettings;
};

View File

@@ -0,0 +1,4 @@
import list from './list';
export * from './applyMigrations.ts';
export const migrations = [...list];

View File

@@ -0,0 +1,5 @@
import { to_1 } from './to_1.ts';
import { to_2 } from './to_2.ts';
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export default [to_1, to_2] as MigrationStructure[];

View File

@@ -0,0 +1,10 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const to_1: MigrationStructure = {
to: 1,
up: (prev: any) => {
return Object.keys(prev).reduce((acc, k) => {
return { ...acc, [k]: prev[k].settings };
}, Object.create(null));
},
};

View File

@@ -0,0 +1,28 @@
import { MigrationStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
const IN_V1_STORE_KEY = 'viewPort';
const IN_V1_DEFAULT_VIEWPORT = { zoom: 1, x: 0, y: 0 };
export const to_2: MigrationStructure = {
to: 2,
up: (prev: any) => {
const restored = localStorage.getItem(IN_V1_STORE_KEY);
let current = IN_V1_DEFAULT_VIEWPORT;
if (restored != null) {
try {
current = JSON.parse(restored);
} catch (err) {
// do nothing
}
localStorage.removeItem(IN_V1_STORE_KEY);
}
return {
...prev,
map: {
viewport: current,
},
};
},
};

View File

@@ -50,36 +50,39 @@ export type RoutesType = {
export type LocalWidgetSettings = {
compact: boolean;
showOffline: boolean;
version: number;
showShipName: boolean;
};
export type OnTheMapSettingsType = {
hideOffline: boolean;
version: number;
};
export type KillsWidgetSettings = {
showAll: boolean;
whOnly: boolean;
excludedSystems: number[];
version: number;
timeRange: number;
};
export type SettingsWithVersion<T> = {
version: number;
settings: T;
export type MapViewPort = { zoom: number; x: number; y: number };
export type MapSettings = {
viewport: MapViewPort;
};
export type SettingsWrapper<T> = T;
export type MapUserSettings = {
widgets: SettingsWithVersion<WindowStoreInfo>;
interface: SettingsWithVersion<InterfaceStoredSettings>;
onTheMap: SettingsWithVersion<OnTheMapSettingsType>;
routes: SettingsWithVersion<RoutesType>;
localWidget: SettingsWithVersion<LocalWidgetSettings>;
signaturesWidget: SettingsWithVersion<SignatureSettingsType>;
killsWidget: SettingsWithVersion<KillsWidgetSettings>;
migratedFromOld: boolean;
version: number;
widgets: SettingsWrapper<WindowStoreInfo>;
interface: SettingsWrapper<InterfaceStoredSettings>;
onTheMap: SettingsWrapper<OnTheMapSettingsType>;
routes: SettingsWrapper<RoutesType>;
localWidget: SettingsWrapper<LocalWidgetSettings>;
signaturesWidget: SettingsWrapper<SignatureSettingsType>;
killsWidget: SettingsWrapper<KillsWidgetSettings>;
map: SettingsWrapper<MapSettings>;
};
export type MapUserSettingsStructure = {
@@ -89,3 +92,20 @@ export type MapUserSettingsStructure = {
export type WdResponse<T> = T;
export type RemoteAdminSettingsResponse = { default_settings?: string };
export enum SettingsTypes {
killsWidget = 'killsWidget',
localWidget = 'localWidget',
widgets = 'widgets',
routes = 'routes',
onTheMap = 'onTheMap',
signaturesWidget = 'signaturesWidget',
interface = 'interface',
map = 'map',
}
export type MigrationFunc = (prev: any) => any;
export type MigrationStructure = {
to: number;
up: MigrationFunc;
};

View File

@@ -0,0 +1,4 @@
export const STORED_SETTINGS_VERSION = 2;
export const LS_KEY_LEGASY = 'map-user-settings';
export const LS_KEY = 'map-user-settings-v2';

View File

@@ -122,7 +122,7 @@ export enum SignatureGroupRU {
export enum SignatureGroupFR {
CosmicSignature = 'Signature cosmique (groupe)',
Wormhole = 'Trou de ver',
GasSite = 'Site de gaz',
GasSite = 'Site de collecte de gaz',
RelicSite = 'Site de reliques',
DataSite = 'Site de données',
OreSite = 'Site de minerai',

View File

@@ -118,6 +118,7 @@ export type SolarSystemRawType = {
name: string | null;
temporary_name: string | null;
linked_sig_eve_id: string | null;
comments_count: number | null;
system_static_info: SolarSystemStaticInfoRaw;
system_signatures: SystemSignature[];

View File

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

View File

@@ -0,0 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const omit = <T extends Record<string, any>, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> => {
const result = { ...obj };
for (const key of keys) {
delete result[key];
}
return result;
};

View File

@@ -4,7 +4,7 @@ import 'phoenix_html';
import './live_reload.css';
const animateBg = function (bgCanvas) {
const { TweenMax, _ } = window;
const { TweenMax } = window;
/**
* Utility function for returning a random integer in a given range
* @param {Int} max
@@ -212,12 +212,6 @@ const animateBg = function (bgCanvas) {
};
}
window.myJump = new JumpToHyperspace(bgCanvas);
window.addEventListener(
'resize',
_.debounce(() => {
window.myJump.reset();
}, 250),
);
};
document.addEventListener('DOMContentLoaded', function () {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/static/favicon-96x96.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

33
assets/static/site.webmanifest Executable file
View File

@@ -0,0 +1,33 @@
{
"name": "Wanderer",
"short_name": "Wanderer",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#171717",
"background_color": "#171717",
"display": "standalone",
"start_url": "/",
"screenshots": [
{
"src": "web-app-manifest.webp",
"sizes": "720x1280",
"type": "image/webp"
},
{
"src": "web-app-manifest-wide.webp",
"sizes": "1280x720",
"type": "image/webp",
"form_factor": "wide"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -203,7 +203,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
end
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(60)
)
@@ -256,7 +256,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
WandererApp.Character.update_character_state(character_id, character_state)
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(30)
)

View File

@@ -24,7 +24,7 @@ defmodule WandererApp.Character.TrackerPool do
@check_location_errors_interval :timer.minutes(1)
@update_ship_interval :timer.seconds(2)
@update_info_interval :timer.minutes(2)
@update_wallet_interval :timer.minutes(1)
@update_wallet_interval :timer.minutes(10)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -180,7 +180,7 @@ defmodule WandererApp.Character.TrackerPool do
fn character_id ->
WandererApp.Character.Tracker.update_online(character_id)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
@@ -241,12 +241,12 @@ defmodule WandererApp.Character.TrackerPool do
WandererApp.Character.Tracker.check_offline(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_offline: #{inspect(reason)}")
error -> @logger.error("Error in check_offline: #{inspect(error)}")
end)
rescue
e ->
@@ -281,12 +281,12 @@ defmodule WandererApp.Character.TrackerPool do
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}")
error -> @logger.error("Error in check_online_errors: #{inspect(error)}")
end)
rescue
e ->
@@ -321,12 +321,12 @@ defmodule WandererApp.Character.TrackerPool do
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_ship_errors: #{inspect(reason)}")
error -> @logger.error("Error in check_ship_errors: #{inspect(error)}")
end)
rescue
e ->
@@ -361,12 +361,12 @@ defmodule WandererApp.Character.TrackerPool do
)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_location_errors: #{inspect(reason)}")
error -> @logger.error("Error in check_location_errors: #{inspect(error)}")
end)
rescue
e ->
@@ -395,7 +395,7 @@ defmodule WandererApp.Character.TrackerPool do
fn character_id ->
WandererApp.Character.Tracker.update_location(character_id)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
@@ -436,7 +436,7 @@ defmodule WandererApp.Character.TrackerPool do
fn character_id ->
WandererApp.Character.Tracker.update_ship(character_id)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(5)
)
@@ -478,12 +478,12 @@ defmodule WandererApp.Character.TrackerPool do
WandererApp.Character.Tracker.update_info(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_info: #{inspect(reason)}")
error -> Logger.error("Error in update_info: #{inspect(error)}")
end)
rescue
e ->
@@ -521,13 +521,13 @@ defmodule WandererApp.Character.TrackerPool do
fn character_id ->
WandererApp.Character.Tracker.update_wallet(character_id)
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
timeout: :timer.minutes(5),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> Logger.error("Error in update_wallet: #{inspect(reason)}")
error -> Logger.error("Error in update_wallet: #{inspect(error)}")
end)
rescue
e ->

View File

@@ -253,7 +253,7 @@ defmodule WandererApp.Esi.ApiClient do
fn destination ->
get_routes(origin, destination, params, opts)
end,
max_concurrency: 20,
max_concurrency: System.schedulers_online() * 4,
timeout: :timer.seconds(30),
on_timeout: :kill_task
)

View File

@@ -254,7 +254,7 @@ defmodule WandererApp.Map.Manager do
:timer.sleep(@maps_start_interval)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.seconds(60)
)

View File

@@ -49,7 +49,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
@logger.error(Exception.message(e))
end
end,
max_concurrency: 10,
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn _ -> :ok end)

View File

@@ -19,6 +19,7 @@ defmodule WandererApp.Map.Operations.Connections do
@medium_ship_size 1
@large_ship_size 2
@xlarge_ship_size 3
@capital_ship_size 4
# System class constants
@c1_system_class 1
@@ -35,6 +36,12 @@ defmodule WandererApp.Map.Operations.Connections do
do_create(attrs, map_id, char_id)
end
def small_ship_size(), do: @small_ship_size
def medium_ship_size(), do: @medium_ship_size
def large_ship_size(), do: @large_ship_size
def freight_ship_size(), do: @xlarge_ship_size
def capital_ship_size(), do: @capital_ship_size
defp do_create(attrs, map_id, char_id) do
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),

View File

@@ -22,7 +22,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
fn acl_id ->
update_acl(acl_id)
end,
max_concurrency: 10,
max_concurrency: System.schedulers_online() * 4,
timeout: :timer.seconds(15)
)
|> Enum.reduce(

View File

@@ -122,14 +122,22 @@ defmodule WandererApp.Map.Server.CharactersImpl do
[]
)
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
if Enum.empty?(invalidate_character_ids) do
:ok
else
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
process_invalidate_characters(invalidate_character_ids, map_id, owner_id, acls)
end
end
defp process_invalidate_characters(invalidate_character_ids, map_id, owner_id, acls) do
invalidate_character_ids
|> Task.async_stream(
fn character_id ->
@@ -166,20 +174,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
end,
timeout: :timer.seconds(60),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, {:remove_character, character_id}} ->
remove_and_untrack_characters(map_id, [character_id])
:ok
|> Enum.reduce([], fn
{:ok, {:remove_character, character_id}}, acc ->
[character_id | acc]
{:ok, _result} ->
:ok
{:ok, _result}, acc ->
acc
{:error, reason} ->
{:error, reason}, acc ->
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
acc
end)
|> case do
[] -> :ok
character_ids_to_remove -> remove_and_untrack_characters(map_id, character_ids_to_remove)
end
end
defp remove_and_untrack_characters(map_id, character_ids) do
@@ -293,7 +305,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
:ok
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task
)
|> Enum.each(fn

View File

@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
require Logger
alias WandererApp.Map.Server.Impl
alias WandererApp.Map.Server.SignaturesImpl
# @ccp1 -1
@c1 1
@@ -214,7 +215,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
),
do:
update_connection(state, :update_time_status, [:time_status], connection_update, fn
%{time_status: old_time_status}, %{id: connection_id, time_status: time_status} ->
%{time_status: old_time_status},
%{id: connection_id, time_status: time_status} = updated_connection ->
case time_status == @connection_time_status_eol do
true ->
if old_time_status != @connection_time_status_eol do
@@ -232,6 +234,10 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
set_start_time(map_id, connection_id, DateTime.utc_now())
end
end
if time_status != old_time_status do
maybe_update_linked_signature_time_status(map_id, updated_connection)
end
end)
def update_connection_type(
@@ -360,10 +366,105 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
state
end
defp maybe_update_linked_signature_time_status(
map_id,
%{
time_status: time_status,
solar_system_source: solar_system_source,
solar_system_target: solar_system_target
} = updated_connection
) do
source_system =
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: solar_system_source}
)
target_system =
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: solar_system_target}
)
source_linked_signatures =
find_linked_signatures(source_system, target_system)
target_linked_signatures = find_linked_signatures(target_system, source_system)
update_signatures_time_status(
map_id,
source_system.solar_system_id,
source_linked_signatures,
time_status
)
update_signatures_time_status(
map_id,
target_system.solar_system_id,
target_linked_signatures,
time_status
)
end
defp find_linked_signatures(
%{id: source_system_id} = _source_system,
%{solar_system_id: solar_system_id, linked_sig_eve_id: linked_sig_eve_id} =
_target_system
)
when not is_nil(linked_sig_eve_id) do
{:ok, signatures} =
WandererApp.Api.MapSystemSignature.by_linked_system_id(solar_system_id)
signatures |> Enum.filter(fn sig -> sig.system_id == source_system_id end)
end
defp find_linked_signatures(_source_system, _target_system), do: []
defp update_signatures_time_status(_map_id, _solar_system_id, [], _time_status), do: :ok
defp update_signatures_time_status(map_id, solar_system_id, signatures, time_status) do
signatures
|> Enum.each(fn %{custom_info: custom_info_json} = sig ->
update_params =
if not is_nil(custom_info_json) do
updated_custom_info =
custom_info_json
|> Jason.decode!()
|> Map.merge(%{"time_status" => time_status})
|> Jason.encode!()
%{custom_info: updated_custom_info}
else
updated_custom_info = Jason.encode!(%{"time_status" => time_status})
%{custom_info: updated_custom_info}
end
SignaturesImpl.apply_update_signature(%{map_id: map_id}, sig, update_params)
end)
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
end
def maybe_add_connection(map_id, location, old_location, character_id, is_manual)
when not is_nil(location) and not is_nil(old_location) and
not is_nil(old_location.solar_system_id) and
location.solar_system_id != old_location.solar_system_id do
{:ok, character} = WandererApp.Character.get_character(character_id)
if not is_manual do
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
end
case WandererApp.Map.check_connection(map_id, location, old_location) do
:ok ->
connection_type =
@@ -429,22 +530,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
time_status: connection.time_status
})
{:ok, character} = WandererApp.Character.get_character(character_id)
if not is_manual do
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
end
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
character_id: character_id,

View File

@@ -17,51 +17,53 @@ defmodule WandererApp.Map.Server.PingsImpl do
user_id: user_id
} = ping_info
) do
{:ok, character} = WandererApp.Character.get_character(character_id)
with {:ok, character} <- WandererApp.Character.get_character(character_id),
system <-
WandererApp.Map.find_system_by_location(map_id, %{
solar_system_id: solar_system_id |> String.to_integer()
}),
{:ok, ping} <-
WandererApp.MapPingsRepo.create(%{
map_id: map_id,
character_id: character_id,
system_id: system.id,
message: message,
type: type
}) do
Impl.broadcast!(
map_id,
:ping_added,
ping |> Map.merge(%{character_eve_id: character.eve_id, solar_system_id: solar_system_id})
)
system =
WandererApp.Map.find_system_by_location(map_id, %{
solar_system_id: solar_system_id |> String.to_integer()
})
# Broadcast rally point events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_added, %{
rally_point_id: ping.id,
solar_system_id: solar_system_id,
system_id: system.id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system.name,
message: message,
created_at: ping.inserted_at
})
end
{:ok, ping} =
WandererApp.MapPingsRepo.create(%{
WandererApp.User.ActivityTracker.track_map_event(:map_rally_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
character_id: character_id,
system_id: system.id,
message: message,
type: type
solar_system_id: "#{solar_system_id}"
})
Impl.broadcast!(
map_id,
:ping_added,
ping |> Map.merge(%{character_eve_id: character.eve_id, solar_system_id: solar_system_id})
)
# Broadcast rally point events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_added, %{
rally_point_id: ping.id,
solar_system_id: solar_system_id,
system_id: system.id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system.name,
message: message,
created_at: ping.inserted_at
})
state
else
error ->
Logger.error("Failed to add_ping: #{inspect(error, pretty: true)}")
state
end
WandererApp.User.ActivityTracker.track_map_event(:map_rally_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: "#{solar_system_id}"
})
state
end
def cancel_ping(
@@ -73,39 +75,42 @@ defmodule WandererApp.Map.Server.PingsImpl do
type: type
} = ping_info
) do
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, %{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} =
WandererApp.MapPingsRepo.get_by_id(ping_id)
:ok = WandererApp.MapPingsRepo.destroy(ping)
Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: solar_system_id,
type: type
})
# Broadcast rally point removal events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
with {:ok, character} <- WandererApp.Character.get_character(character_id),
{:ok,
%{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} <-
WandererApp.MapPingsRepo.get_by_id(ping_id),
:ok <- WandererApp.MapPingsRepo.destroy(ping) do
Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: solar_system_id,
system_id: system_id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system_name
type: type
})
# Broadcast rally point removal events to external clients (webhooks/SSE)
if type == 1 do
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
id: ping_id,
solar_system_id: solar_system_id,
system_id: system_id,
character_id: character_id,
character_name: character.name,
character_eve_id: character.eve_id,
system_name: system_name
})
end
WandererApp.User.ActivityTracker.track_map_event(:map_rally_cancelled, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: solar_system_id
})
state
else
error ->
Logger.error("Failed to cancel_ping: #{inspect(error, pretty: true)}")
state
end
WandererApp.User.ActivityTracker.track_map_event(:map_rally_cancelled, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: solar_system_id
})
state
end
end

View File

@@ -7,6 +7,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
alias WandererApp.Character
alias WandererApp.User.ActivityTracker
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
alias WandererApp.Utils.EVEUtil
@doc """
Public entrypoint for updating signatures on a map system.
@@ -92,7 +93,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|> Enum.filter(&(&1.eve_id in updated_ids))
|> Enum.each(fn existing ->
update = Enum.find(updated_sigs, &(&1.eve_id == existing.eve_id))
apply_update_signature(existing, update)
apply_update_signature(state, existing, update)
end)
# 3. Additions & restorations
@@ -209,13 +210,19 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
MapSystemSignature.update!(sig, %{deleted: true})
end
defp apply_update_signature(%MapSystemSignature{} = existing, update_params)
when not is_nil(update_params) do
def apply_update_signature(
state,
%MapSystemSignature{} = existing,
update_params
)
when not is_nil(update_params) do
case MapSystemSignature.update(
existing,
update_params |> Map.put(:update_forced_at, DateTime.utc_now())
) do
{:ok, _updated} ->
{:ok, updated} ->
maybe_update_connection_time_status(state, existing, updated)
maybe_update_connection_mass_status(state, existing, updated)
:ok
{:error, reason} ->
@@ -223,6 +230,52 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
end
end
defp maybe_update_connection_time_status(
state,
%{custom_info: old_custom_info} = old_sig,
%{custom_info: new_custom_info, system_id: system_id, linked_system_id: linked_system_id} =
updated_sig
)
when not is_nil(linked_system_id) do
old_time_status = get_time_status(old_custom_info)
new_time_status = get_time_status(new_custom_info)
if old_time_status != new_time_status do
{:ok, source_system} = MapSystem.by_id(system_id)
ConnectionsImpl.update_connection_time_status(state, %{
solar_system_source_id: source_system.solar_system_id,
solar_system_target_id: linked_system_id,
time_status: new_time_status
})
end
end
defp maybe_update_connection_time_status(_state, _old_sig, _updated_sig), do: :ok
defp maybe_update_connection_mass_status(
state,
%{type: old_type} = old_sig,
%{type: new_type, system_id: system_id, linked_system_id: linked_system_id} =
updated_sig
)
when not is_nil(linked_system_id) do
if old_type != new_type do
{:ok, source_system} = MapSystem.by_id(system_id)
signature_ship_size_type = EVEUtil.get_wh_size(new_type)
if not is_nil(signature_ship_size_type) do
ConnectionsImpl.update_connection_ship_size_type(state, %{
solar_system_source_id: source_system.solar_system_id,
solar_system_target_id: linked_system_id,
ship_size_type: signature_ship_size_type
})
end
end
end
defp maybe_update_connection_mass_status(_state, _old_sig, _updated_sig), do: :ok
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
ActivityTracker.track_map_event(event, %{
map_id: map_id,
@@ -251,4 +304,12 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
}
end)
end
defp get_time_status(nil), do: nil
defp get_time_status(custom_info_json) do
custom_info_json
|> Jason.decode!()
|> Map.get("time_status")
end
end

View File

@@ -41,7 +41,7 @@ defmodule WandererApp.Maps do
system |> Map.take(@minimum_route_attrs)
end
end,
max_concurrency: 10
max_concurrency: System.schedulers_online() * 4
)
|> Enum.map(fn {:ok, val} -> val end)

View File

@@ -3,6 +3,8 @@ defmodule WandererApp.Utils.EVEUtil do
Utility functions for EVE Online related operations.
"""
alias WandererApp.Map.Operations.Connections
@doc """
Generates a URL for a character portrait.
@@ -28,4 +30,28 @@ defmodule WandererApp.Utils.EVEUtil do
def get_portrait_url(eve_id, size) do
"https://images.evetech.net/characters/#{eve_id}/portrait?size=#{size}"
end
def get_wh_size(nil), do: nil
def get_wh_size("K162"), do: nil
def get_wh_size(wh_type_name) do
{:ok, wormhole_types} = WandererApp.CachedInfo.get_wormhole_types()
wormhole_types
|> Enum.find(fn wh_type_data -> wh_type_data.name == wh_type_name end)
|> case do
%{max_mass_per_jump: max_mass_per_jump} when not is_nil(max_mass_per_jump) ->
get_connection_size_status(max_mass_per_jump)
_ ->
nil
end
end
defp get_connection_size_status(5_000_000), do: Connections.small_ship_size()
defp get_connection_size_status(62_000_000), do: Connections.medium_ship_size()
defp get_connection_size_status(375_000_000), do: Connections.large_ship_size()
defp get_connection_size_status(1_000_000_000), do: Connections.freight_ship_size()
defp get_connection_size_status(2_000_000_000), do: Connections.capital_ship_size()
defp get_connection_size_status(_max_mass_per_jump), do: Connections.large_ship_size()
end

View File

@@ -17,7 +17,9 @@ defmodule WandererAppWeb do
those modules here.
"""
def static_paths, do: ~w(assets fonts images icons favicon.ico robots.txt woff woff2 lottie)
def static_paths,
do:
~w(assets fonts images icons favicon.ico site.webmanifest apple-touch-icon.png web-app-manifest-192x192.png web-app-manifest-512x512.png web-app-manifest.webp web-app-manifest-wide.webp robots.txt woff woff2 lottie)
def router do
quote do

View File

@@ -8,6 +8,9 @@
{assigns[:page_title] || "Welcome"}
</.live_title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<link

View File

@@ -80,6 +80,11 @@ defmodule WandererAppWeb.MapSystemAPIController do
properties: %{
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
custom_name: %Schema{
type: :string,
nullable: true,
description: "Custom name for the system"
},
position_x: %Schema{type: :integer, description: "X coordinate"},
position_y: %Schema{type: :integer, description: "Y coordinate"},
status: %Schema{
@@ -98,6 +103,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
example: %{
solar_system_id: 30_000_142,
solar_system_name: "Jita",
custom_name: "Trade Hub",
position_x: 100,
position_y: 200,
visible: true,
@@ -113,6 +119,11 @@ defmodule WandererAppWeb.MapSystemAPIController do
description: "EVE solar system name",
nullable: true
},
custom_name: %Schema{
type: :string,
nullable: true,
description: "Custom name for the system"
},
position_x: %Schema{type: :integer, description: "X coordinate", nullable: true},
position_y: %Schema{type: :integer, description: "Y coordinate", nullable: true},
status: %Schema{
@@ -130,6 +141,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
},
example: %{
solar_system_name: "Jita",
custom_name: "Trade Hub",
position_x: 101,
position_y: 202,
visible: false,

View File

@@ -118,6 +118,7 @@ defmodule WandererAppWeb.Helpers.APIUtils do
optional = [
"solar_system_name",
"custom_name",
"position_x",
"position_y",
"coordinates",
@@ -151,6 +152,7 @@ defmodule WandererAppWeb.Helpers.APIUtils do
def extract_update_params(params) when is_map(params) do
allowed = [
"solar_system_name",
"custom_name",
"position_x",
"position_y",
"coordinates",

View File

@@ -339,7 +339,7 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
destination_id
)
end,
max_concurrency: System.schedulers_online(),
max_concurrency: System.schedulers_online() * 4,
on_timeout: :kill_task,
timeout: :timer.minutes(1)
)

View File

@@ -4,6 +4,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
require Logger
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
alias WandererApp.Utils.EVEUtil
def handle_server_event(
%{
@@ -178,44 +179,69 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
}
} = socket
)
when not is_nil(main_character_id) do
when not is_nil(main_character_id) and not is_nil(solar_system_source) and
not is_nil(solar_system_target) do
with solar_system_source <- get_integer(solar_system_source),
solar_system_target <- get_integer(solar_system_target),
{:ok, source_system} <-
WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: solar_system_source
}),
signature <-
source_system when not is_nil(source_system) <-
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: solar_system_source}
),
signature when not is_nil(signature) <-
WandererApp.Api.MapSystemSignature.by_system_id!(source_system.id)
|> Enum.find(fn s -> s.eve_id == signature_eve_id end),
target_system <-
target_system when not is_nil(target_system) <-
WandererApp.Map.find_system_by_location(
map_id,
%{solar_system_id: solar_system_target}
) do
if not is_nil(signature) do
signature
|> WandererApp.Api.MapSystemSignature.update_group!(%{group: "Wormhole"})
|> WandererApp.Api.MapSystemSignature.update_linked_system(%{
linked_system_id: solar_system_target
signature
|> WandererApp.Api.MapSystemSignature.update_group!(%{group: "Wormhole"})
|> WandererApp.Api.MapSystemSignature.update_linked_system(%{
linked_system_id: solar_system_target
})
if is_nil(target_system.linked_sig_eve_id) do
map_id
|> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
solar_system_id: solar_system_target,
linked_sig_eve_id: signature_eve_id
})
if not is_nil(target_system) &&
is_nil(target_system.linked_sig_eve_id) do
if not is_nil(signature.temporary_name) do
map_id
|> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
|> WandererApp.Map.Server.update_system_temporary_name(%{
solar_system_id: solar_system_target,
linked_sig_eve_id: signature_eve_id
temporary_name: signature.temporary_name
})
end
if not is_nil(signature.temporary_name) do
map_id
|> WandererApp.Map.Server.update_system_temporary_name(%{
solar_system_id: solar_system_target,
temporary_name: signature.temporary_name
})
signature_time_status =
if not is_nil(signature.custom_info) do
signature.custom_info |> Jason.decode!() |> Map.get("time_status")
else
nil
end
if not is_nil(signature_time_status) do
map_id
|> WandererApp.Map.Server.update_connection_time_status(%{
solar_system_source_id: solar_system_source,
solar_system_target_id: solar_system_target,
time_status: signature_time_status
})
end
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
if not is_nil(signature_ship_size_type) do
map_id
|> WandererApp.Map.Server.update_connection_ship_size_type(%{
solar_system_source_id: solar_system_source,
solar_system_target_id: solar_system_target,
ship_size_type: signature_ship_size_type
})
end
end

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.78.1"
@version "1.81.5"
def project do
[

BIN
priv/static/apple-touch-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
priv/static/favicon-96x96.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

33
priv/static/site.webmanifest Executable file
View File

@@ -0,0 +1,33 @@
{
"name": "Wanderer",
"short_name": "Wanderer",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#171717",
"background_color": "#171717",
"display": "standalone",
"start_url": "/",
"screenshots": [
{
"src": "web-app-manifest.webp",
"sizes": "720x1280",
"type": "image/webp"
},
{
"src": "web-app-manifest-wide.webp",
"sizes": "1280x720",
"type": "image/webp",
"form_factor": "wide"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -127,6 +127,31 @@ defmodule WandererAppWeb.MapSystemAPIControllerSuccessTest do
assert updated_system["position_y"] == 400.0
end
test "UPDATE: successfully updates custom_name", %{conn: conn, map: map} do
system =
insert(:map_system, %{
map_id: map.id,
solar_system_id: 30_000_142,
name: "Jita",
position_x: 100,
position_y: 200
})
update_params = %{
"custom_name" => "My Trade Hub"
}
conn = put(conn, ~p"/api/maps/#{map.slug}/systems/#{system.id}", update_params)
response = json_response(conn, 200)
assert %{
"data" => updated_system
} = response
assert updated_system["custom_name"] == "My Trade Hub"
end
test "DELETE: successfully deletes a system", %{conn: conn, map: map} do
system =
insert(:map_system, %{