Compare commits

...

68 Commits

Author SHA1 Message Date
CI
b1149cecaf chore: release version v1.37.6
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-09 21:57:08 +00:00
Aleksei Chichenkov
8f28d2be65 Merge pull request #111 from guarzo/guarzo/themefixes
fix: support additional theme names
2025-01-10 00:56:39 +03:00
Gustav
d758b54ef8 fix: support additional theme names 2025-01-09 15:06:32 -05:00
Gustav
58293b4dc4 refactor: providing some additional variables for theme support 2025-01-09 14:24:03 -05:00
CI
f2083f4256 chore: release version v1.37.5
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-09 19:09:14 +00:00
Aleksei Chichenkov
6c7bd5804e Merge pull request #109 from guarzo/guarzo/themefixes
fix: restore node styling, simplify framework for new themes
2025-01-09 22:08:47 +03:00
Gustav
483ae21e89 fix: restore node styling, simplify framework for new themes 2025-01-09 13:38:11 -05:00
CI
f734565844 chore: release version v1.37.4 2025-01-09 13:21:32 +00:00
CI
8c718ba181 chore: release version v1.37.3 2025-01-09 12:52:30 +00:00
Aleksei Chichenkov
c8d8734601 Merge pull request #108 from wanderer-industries/fix-dbclick
fix(Map): Fixed dbclick behaviour
2025-01-09 15:51:16 +03:00
achichenkov
5c757e8255 fix(Map): Fixed dbclick behaviour 2025-01-09 15:50:03 +03:00
CI
82f90ef759 chore: release version v1.37.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-09 07:26:08 +00:00
Aleksei Chichenkov
167c8eea6b Merge pull request #107 from guarzo/guarzo/theme-pathfinder
fix: fix route color
2025-01-09 10:25:37 +03:00
Gustav
d76079d4c7 theme cleanup 2025-01-08 23:45:27 -05:00
Gustav
bf9c4cda02 route color fix 2025-01-08 23:44:12 -05:00
CI
af00402546 chore: release version v1.37.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-08 21:53:07 +00:00
Aleksei Chichenkov
a245842ca4 Merge pull request #106 from guarzo/guarzo/themes
refactor: cleaned up scss classes, and updated theme dropdown to use declarative style
2025-01-09 00:52:36 +03:00
Gustav
8ddd672f13 fix: add back pathfinder theme font 2025-01-08 16:44:23 -05:00
Gustav
92f471c0b0 refactor: declarative dropdown for theme choice 2025-01-08 16:20:34 -05:00
Gustav
9e2a2c5b44 chore: clean up theme scss class usage 2025-01-08 16:20:25 -05:00
CI
5f5d3df003 chore: release version v1.37.0 2025-01-08 20:01:55 +00:00
Aleksei Chichenkov
c66cc8868e Merge pull request #100 from guarzo/guarzo/themes
feat: add theme selection, and pathfinder theme
2025-01-08 23:01:26 +03:00
guarzo
0d6528ce4f Merge branch 'main' into guarzo/themes 2025-01-08 14:05:59 -05:00
CI
34c385ac5f chore: release version v1.36.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-08 17:55:41 +00:00
Aleksei Chichenkov
b6d12e73a9 Merge pull request #105 from wanderer-industries/feat-wnd-104-fix-pasting
fix(Map): Fixed pasting into Name, Custom Label and Description
2025-01-08 20:55:16 +03:00
achichenkov
1118858120 fix(Map): Fixed pasting into Name, Custom Label and Description 2025-01-08 20:35:57 +03:00
CI
ae3a34d5bf chore: release version v1.36.1 2025-01-08 17:08:14 +00:00
Aleksei Chichenkov
43df42e49b Merge pull request #102 from wanderer-industries/feat-signatures-ru
fix(Map): Add support RU signatures and fix filtering
2025-01-08 20:07:43 +03:00
achichenkov
e670f3bf03 fix(Map): Removed unnecessary comment 2025-01-08 19:39:33 +03:00
achichenkov
c26a9404c5 fix(Map): Add support RU signatures and fix filtering 2025-01-08 12:46:56 +03:00
Gustav
c0fad4ca92 unused import 2025-01-07 18:44:49 -05:00
Gustav
16dbf9378b feat: add theme selection and pathfinder theme 2025-01-07 18:44:46 -05:00
CI
4001fe5eac chore: release version v1.36.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-07 23:28:11 +00:00
guarzo
2992dd8f8b feat: added static system info to api (#101)
* feat: added static system info to api
2025-01-08 03:27:44 +04:00
CI
98a03d1e59 chore: release version v1.35.0 2025-01-07 23:05:04 +00:00
guarzo
2088393c79 feat(Map): add "temporary system names" toggle (#86)
If enabled and set, a temporary name is displayed instead of the system name. The original system name appears on a secondary row if no custom name exists. Temporary names are removed when the system is removed from the map.
2025-01-08 03:04:36 +04:00
CI
093042b88a chore: release version v1.33.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-07 12:47:07 +00:00
Dmitry Popov
e5ef35c186 hotfix: Fix map behaviour for 'Allow only tracked characters' map option 2025-01-07 13:46:34 +01:00
CI
1cd23d5efd chore: release version v1.33.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-07 09:17:09 +00:00
guarzo
ead5818a3f feat(Map): api to allow systematic access to visible systems and tracked characters (#89)
* feat: add limited api for system and tracked characters
* env variable to disable public api key
2025-01-07 13:16:39 +04:00
CI
a5f66ada68 chore: release version v1.32.7
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-06 22:24:26 +00:00
Dmitry Popov
0919742853 Revert "hotfix: add ARM64 compatible Docker image (#94)" (#99)
This reverts commit f85317983c.
2025-01-07 02:23:59 +04:00
CI
f3efffd259 chore: release version v1.32.6 2025-01-06 21:56:57 +00:00
Tsuro Tsero
f85317983c hotfix: add ARM64 compatible Docker image (#94) 2025-01-07 01:56:22 +04:00
CI
76f709b768 chore: release version v1.32.5
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-04 20:23:09 +00:00
guarzo
e3b2356302 fix(map): prevent deselect on click to map (#96)
Fixes #80

- Prevents single node deselection on background / same node click
- Allows deseletion of all nodes if multiple are currently selected
2025-01-05 00:22:41 +04:00
CI
3d810211ee chore: release version v1.32.4
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-02 19:56:48 +00:00
Dmitry Popov
7453795dc5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-02 20:56:18 +01:00
Dmitry Popov
9de7cd99ee fix(Map): Fix 'Character Activity' modal 2025-01-02 20:56:14 +01:00
CI
51489c1aa5 chore: release version v1.32.3 2025-01-02 17:23:05 +00:00
Dmitry Popov
25dd6de770 fix(Map): Fix 'Allow only tracked characters' saving 2025-01-02 18:22:32 +01:00
CI
2a825f5a02 chore: release version v1.32.2
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-02 10:12:04 +00:00
guarzo
908d249eb9 Allow user to follow a specific tracked character (#87)
* add follow character functionality
2025-01-02 14:09:10 +04:00
CI
6cd119e8f4 chore: release version v1.32.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-25 13:34:54 +00:00
Dmitry Popov
9a59c8eb75 Merge pull request #81 from LordMrcS/main
Update Wormhole Path Colors for Reduced and VOC
2024-12-25 17:34:30 +04:00
Marcos Silva
452c022d41 Merge branch 'main' into main 2024-12-25 10:03:04 -03:00
CI
27e9bab82a chore: release version v1.32.0
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2024-12-24 09:02:07 +00:00
Aleksei Chichenkov
edef860530 Merge pull request #85 from wanderer-industries/search-systems
Improve Search systems for Routes and Map (Manually add system)
2024-12-24 12:01:40 +03:00
achichenkov
032cb63411 Merge branch 'refs/heads/main' into search-systems 2024-12-24 11:43:02 +03:00
achichenkov
a1791ba578 fix(Map): Added ability to add new system to routes via routes widget 2024-12-24 11:42:46 +03:00
Dmitry Popov
3a69fd7786 chore(Map): Get rid of old add system modal 2024-12-23 11:13:49 +01:00
achichenkov
8a90723c2e fix(Map): Reworked add system to map 2024-12-23 13:06:15 +03:00
Dmitry Popov
af2fc342c7 chore(Map): Add search systems 2024-12-23 10:44:46 +01:00
Dmitry Popov
05ea2fcdbe chore(Map): Add default filtering to search systems 2024-12-22 22:59:28 +01:00
Dmitry Popov
6d4321fead chore(Map): Add static info to search systems 2024-12-22 17:16:42 +01:00
Dmitry Popov
0796bcf7d0 feat(Map): Add search & update manual adding systems API 2024-12-18 11:13:20 +01:00
Dmitry Popov
0b5bec142a feat(Map): Add search & update manual adding systems API 2024-12-18 11:09:52 +01:00
Marcos Silva
e0a37f7635 Update Wormhole Path Colors for Reduced and VOC
Replaced the reduced and verge of collapse path colors to make it clearer.
2024-12-14 20:03:30 -03:00
85 changed files with 4760 additions and 1599 deletions

View File

@@ -6,3 +6,4 @@ export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"
export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
export GIT_SHA="1111"
export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false"

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@
@import 'primereact/resources/themes/arya-blue/theme.css' layer(primereact);
/*@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css' layer(primereact);*/
@import '../js/hooks/Mapper/components/map/styles/index.scss';
@layer tailwind-base {
@tailwind base;
}

View File

@@ -108,3 +108,7 @@
.p-dropdown-empty-message {
padding: 0.25rem 0.5rem;
}
.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token {
margin-right: 0 !important;
}

View File

@@ -88,6 +88,23 @@ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseC
setSystem(undefined);
}, []);
const onSystemTemporaryName = useCallback((temporaryName?: string) => {
const { system, outCommand } = ref.current;
if (!system) {
return;
}
outCommand({
type: OutCommand.updateSystemTemporaryName,
data: {
system_id: system,
value: temporaryName ?? '',
},
});
setSystem(undefined);
}, []);
const onSystemStatus = useCallback((status: number) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -161,6 +178,7 @@ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseC
onLockToggle,
onHubToggle,
onSystemTag,
onSystemTemporaryName,
onSystemStatus,
onSystemLabels,
onOpenSettings,

View File

@@ -0,0 +1,173 @@
// feel free to rename these imports or the file path as you see fit
import { useMemo } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick.ts';
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { MapSolarSystemType } from '../../map.types';
import {
LABELS_INFO,
LABELS_ORDER,
getActivityType,
} from '@/hooks/Mapper/components/map/constants.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { OutCommand } from '@/hooks/Mapper/types';
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
};
const sortedLabels = (labels: string[]) => {
if (!labels) return [];
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
};
interface UseSolarSystemNodeParams {
data: MapSolarSystemType;
selected: boolean;
}
export function useSolarSystemNode({ data, selected }: UseSolarSystemNodeParams) {
// 1) Bring in relevant global state
const { interfaceSettings } = useMapRootState();
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const {
data: {
characters,
presentCharacters,
wormholesData,
hubs,
kills,
userCharacters,
isConnecting,
hoverNodeId,
visibleNodes,
showKSpaceBG,
isThickConnections,
},
outCommand,
} = useMapState();
// 2) Extract data from the node
const {
system_class,
security,
class_title,
solar_system_id,
statics,
effect_name,
region_name,
region_id,
is_shattered,
solar_system_name,
} = data.system_static_info;
const { locked, name, tag, status, labels, id, temporary_name: temporaryName } = data || {};
const signatures = data.system_signatures;
// 3) Compute derived values
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const charactersInSystem = useMemo(() => {
return characters
.filter(c => c.location?.solar_system_id === solar_system_id)
.filter(c => c.online);
}, [characters, presentCharacters, solar_system_id]);
const isWormhole = isWormholeSpace(system_class);
const classTitleColor = useMemo(
() => getSystemClassStyles({ systemClass: system_class, security }),
[security, system_class],
);
const sortedStatics = useMemo(
() => sortWHClasses(wormholesData, statics),
[wormholesData, statics],
);
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
const labelCustom = useMemo(() => labelsManager.customLabel, [labelsManager]);
const killsCount = useMemo(() => {
const systemKills = kills[solar_system_id];
if (!systemKills) return null;
return systemKills;
}, [kills, solar_system_id]);
const hasUserCharacters = useMemo(() => {
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
}, [charactersInSystem, userCharacters]);
const dbClick = useDoubleClick(() => {
outCommand({
type: OutCommand.openSettings,
data: {
system_id: solar_system_id.toString(),
},
});
});
const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const systemName = (isTempSystemNameEnabled && temporaryName) || solar_system_name;
const customName = (isTempSystemNameEnabled && temporaryName && name)
|| (solar_system_name !== name && name);
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
}
return prepareUnsplashedChunks(
signatures
.filter(s => s.group === 'Wormhole' && !s.linked_system)
.map(s => ({
eve_id: s.eve_id,
type: s.type,
custom_info: s.custom_info,
})),
);
}, [isShowUnsplashedSignatures, signatures]);
return {
selected,
visible,
isWormhole,
classTitleColor,
killsCount,
hasUserCharacters,
showHandlers,
regionClass,
systemName,
customName,
labelCustom,
is_shattered,
tag,
status,
labelsInfo,
dbClick,
sortedStatics,
effect_name,
region_name,
solar_system_id,
locked,
hubs,
charactersInSystem,
unsplashedLeft,
unsplashedRight,
isThickConnections,
};
}

View File

@@ -1,8 +1,11 @@
.MapRoot {
width: 100%;
height: 100%;
}
.BackgroundAlternateColor {
background-color: var(--rf-bg-color, #000000);
&.BackgroundAlternateColor {
background-color: var(--rf-soft-bg-color, #171717);
--rf-node-bg-color: var(--rf-node-soft-bg-color, #2b2b2b);
}
}

View File

@@ -1,4 +1,4 @@
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect } from 'react';
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
@@ -16,8 +16,6 @@ import ReactFlow, {
} from 'reactflow';
import 'reactflow/dist/style.css';
import classes from './Map.module.scss';
import './styles/neon-theme.scss';
import './styles/eve-common.scss';
import { MapProvider, useMapState } from './MapProvider';
import { useNodesState, useEdgesState, useMapHandlers, useUpdateNodes } from './hooks';
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
@@ -25,16 +23,19 @@ import {
ContextMenuConnection,
ContextMenuRoot,
SolarSystemEdge,
SolarSystemNode,
SolarSystemNodeDefault,
SolarSystemNodeTheme,
useContextMenuConnectionHandlers,
useContextMenuRootHandlers,
} from './components';
import { OnMapSelectionChange } from './map.types';
import { wrapNode } from './utils/wrapNode';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import clsx from 'clsx';
import { useBackgroundVars } from './hooks/useBackgroundVars';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
@@ -76,11 +77,8 @@ const initialEdges = [
},
];
const nodeTypes = {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
custom: SolarSystemNode,
} as never;
const edgeTypes = {
floating: SolarSystemEdge,
@@ -92,6 +90,7 @@ interface MapCompProps {
onSelectionChange: OnMapSelectionChange;
onManualDelete(systems: string[]): void;
onConnectionInfoClick?(e: SolarSystemConnection): void;
onAddSystem?: OnMapAddSystemCallback;
onSelectionContextMenu?: NodeSelectionMouseHandler;
minimapClasses?: string;
isShowMinimap?: boolean;
@@ -100,6 +99,7 @@ interface MapCompProps {
isThickConnections?: boolean;
isShowBackgroundPattern?: boolean;
isSoftBackground?: boolean;
theme?: string;
}
const MapComp = ({
@@ -116,16 +116,30 @@ const MapComp = ({
isThickConnections,
isShowBackgroundPattern,
isSoftBackground,
theme,
onAddSystem,
}: MapCompProps) => {
const { getNode } = useReactFlow();
const { getNode, getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
const nodeTypes = useMemo(() => {
return {
custom:
theme !== '' && theme !== 'default'
? wrapNode(SolarSystemNodeTheme)
: wrapNode(SolarSystemNodeDefault),
};
}, [theme]);
useMapHandlers(refn, onSelectionChange);
useUpdateNodes(nodes);
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers();
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
const { update } = useMapState();
const { variant, gap, size, color } = useBackgroundVars(theme);
const onConnect: OnConnect = useCallback(
params => {
@@ -184,6 +198,12 @@ const MapComp = ({
(changes: NodeChange[]) => {
const systemsIdsToRemove: string[] = [];
// prevents single node deselection on background / same node click
// allows deseletion of all nodes if multiple are currently selected
if (changes.length === 1 && changes[0].type == 'select' && changes[0].selected === false) {
changes[0].selected = getNodes().filter(node => node.selected).length === 1;
}
const nextChanges = changes.reduce((acc, change) => {
if (change.type !== 'remove') {
return [...acc, change];
@@ -221,7 +241,7 @@ const MapComp = ({
return (
<>
<div className={clsx(classes.MapRoot, { ['bg-neutral-900']: isSoftBackground })}>
<div className={clsx(classes.MapRoot, { [classes.BackgroundAlternateColor]: isSoftBackground })}>
<ReactFlow
nodes={nodes}
edges={edges}
@@ -268,7 +288,7 @@ const MapComp = ({
selectionMode={SelectionMode.Partial}
>
{isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
{isShowBackgroundPattern && <Background />}
{isShowBackgroundPattern && <Background variant={variant} gap={gap} size={size} color={color} />}
</ReactFlow>
{/* <button className="z-auto btn btn-primary absolute top-20 right-20" onClick={handleGetPassages}>
Test // DON NOT REMOVE

View File

@@ -1,15 +1,17 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.ConnectionTimeEOL {
background-image: linear-gradient(207deg, transparent, #7452c3e3);
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
}
.ConnectionFrigate {
background-image: linear-gradient(207deg, transparent, #325d88);
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
}
.ConnectionSave {
background-image: linear-gradient(207deg, transparent, rgba(155, 102, 45, 0.85));
background-image: linear-gradient(207deg, transparent, var(--conn-save));
}
.SelectedItem {
background-color: rgba(98, 98, 98, 0.33);
background-color: var(--selected-item-bg);
}

View File

@@ -1,14 +1,16 @@
import { useReactFlow, XYPosition } from 'reactflow';
import React, { useRef, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { useMapState } from '../../MapProvider.tsx';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
export const useContextMenuRootHandlers = () => {
type UseContextMenuRootHandlers = {
onAddSystem?: OnMapAddSystemCallback;
};
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
const rf = useReactFlow();
const contextMenuRef = useRef<ContextMenu | null>(null);
const { outCommand } = useMapState();
const [position, setPosition] = useState<XYPosition | null>(null);
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -18,14 +20,17 @@ export const useContextMenuRootHandlers = () => {
contextMenuRef.current?.show(e);
};
const onAddSystem = () => {
outCommand({ type: OutCommand.manualAddSystem, data: { coordinates: position } });
};
const ref = useRef({ onAddSystem, position });
ref.current = { onAddSystem, position };
const onAddSystemCallback = useCallback(() => {
ref.current.onAddSystem?.({ coordinates: position });
}, [position]);
return {
handleRootContext,
contextMenuRef,
onAddSystem,
onAddSystem: onAddSystemCallback,
};
};

View File

@@ -1,23 +1,47 @@
@import "@/hooks/Mapper/components/map/styles/neon-variables";
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
.react-flow__edge.selected {
.EdgePathBack {
stroke: $pastel-yellow;
.EdgePathBack {
fill: none;
stroke: #80a5c5;
stroke-width: 3px;
&.TimeCrit {
stroke: #f11ab2;
stroke-width: 4px;
}
&.Hovered {
stroke: #b5c8d9;
&.TimeCrit {
stroke: #ef7dce;
}
}
&.Tick {
stroke-width: 5px;
&.TimeCrit {
stroke-width: 6px;
}
}
&.Gate {
stroke: #9aff40;
}
}
.EdgePathFront {
fill: none;
stroke: #2c3844;
stroke-width: 2px;
&.MassVerge:not(&.Frigate) {
stroke: #af2900;
stroke: #af0000;
}
&.MassHalf:not(&.Frigate) {
stroke: #a85f00;
stroke: #ffd700;
}
&.Frigate {
@@ -54,46 +78,29 @@
}
}
.EdgePathBack {
fill: none;
stroke: #80a5c5;
stroke-width: 3px;
&.TimeCrit {
stroke: #f11ab2;
stroke-width: 4px;
}
&.Hovered {
stroke: #b5c8d9;
&.TimeCrit {
stroke: #ef7dce;
}
}
&.Tick {
stroke-width: 5px;
&.TimeCrit {
stroke-width: 6px;
}
}
&.Gate {
stroke: #9aff40;
}
}
.ClickPath {
fill: none;
stroke: none;
stroke-width: 8px;
}
.LinkLabel{
.Handle {
border: 1px solid var(--pastel-blue);
width: 5px;
height: 5px;
z-index: 1001;
&.Tick {
width: 7px;
height: 7px;
}
&.Right {
margin-left: 0px;
}
}
.LinkLabel {
font-size: 9px;
line-height: 10px;
padding: 2px 4px;
@@ -109,22 +116,3 @@
height: 8px;
font-size: 8px;
}
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid #5a7d9a;
width: 5px;
height: 5px;
z-index: 1001;
&.Tick {
width: 7px;
height: 7px;
&.Right {
margin-left: 0px;
}
}
}

View File

@@ -130,7 +130,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
/>
<div
className="absolute flex items-center gap-1"
className="absolute flex items-center gap-1 pointer-events-none"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
}}

View File

@@ -4,6 +4,7 @@ import { MapSolarSystemType } from '../../map.types';
import classes from './SolarSystemNode.module.scss';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import {
EFFECT_BACKGROUND_STYLES,
@@ -56,6 +57,8 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const { interfaceSettings } = useMapRootState();
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const {
system_class,
security,
@@ -71,18 +74,14 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const signatures = data.system_signatures;
const { locked, name, tag, status, labels, id } = data || {};
const customName = solar_system_name !== name ? name : undefined;
const { locked, name, tag, status, labels, id, temporary_name: temporaryName } = data || {};
const {
data: {
characters,
presentCharacters,
wormholesData,
hubs,
kills,
userCharacters,
isConnecting,
hoverNodeId,
visibleNodes,
@@ -96,8 +95,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const charactersInSystem = useMemo(() => {
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
// eslint-disable-next-line
}, [characters, presentCharacters, solar_system_id]);
}, [characters, solar_system_id]);
const isWormhole = isWormholeSpace(system_class);
const classTitleColor = useMemo(
@@ -114,14 +112,9 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
if (!systemKills) {
return null;
}
return systemKills;
}, [kills, solar_system_id]);
const hasUserCharacters = useMemo(() => {
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
}, [charactersInSystem, userCharacters]);
const dbClick = useDoubleClick(() => {
outCommand({
type: OutCommand.openSettings,
@@ -131,16 +124,58 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
});
});
const showHandlers = isConnecting || hoverNodeId === id;
const dropHandler = isConnecting ? 'all' : 'none';
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const hasTempName = isTempSystemNameEnabled && temporaryName;
// systemName: if temporary name is enabled and present, use it; otherwise use solar_system_name
const systemName = hasTempName
? temporaryName
: solar_system_name;
// hsCustomLabel: if temporary name is enabled and present, show region_name, otherwise labelCustom
const hsCustomLabel = hasTempName
? region_name
: labelCustom;
// whCustomLabel: default to solar_system_name; if that's falsy, use labelCustom
const whCustomLabel = solar_system_name || labelCustom;
// customLabel: if wormhole, use whCustomLabel; otherwise hsCustomLabel
const customLabel = isWormhole
? whCustomLabel
: hsCustomLabel;
// whCustomName: if name differs from solar_system_name, use name; otherwise blank
const whCustomName = (name !== solar_system_name)
? name
: '';
// hsSuffix: if name differs from solar_system_name, append name; otherwise blank
const needsHsSuffix = (name !== solar_system_name);
const hsSuffix = needsHsSuffix
? name
: '';
// hsCustomName: if there's a temp name, show "solar_system_name + suffix", otherwise "region_name + suffix"
const hsCustomName = hasTempName
? `${solar_system_name} ${hsSuffix}`
: `${region_name} ${hsSuffix}`;
// customName: if wormhole, use whCustomName; otherwise hsCustomName
const customName = isWormhole
? whCustomName
: hsCustomName;
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
}
return prepareUnsplashedChunks(
signatures
.filter(s => s.group === 'Wormhole' && !s.linked_system)
@@ -156,22 +191,22 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
<>
{visible && (
<div className={classes.Bookmarks}>
{labelCustom !== '' && (
{customLabel !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{customLabel}</span>
</div>
)}
{is_shattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
<span className={clsx('pi pi-chart-pie text-[0.55rem]')} />
</div>
)}
{killsCount && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[getActivityType(killsCount)])}>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(PrimeIcons.BOLT, 'text-[0.65rem]')} />
<span className={clsx(classes.text)}>{killsCount}</span>
</div>
</div>
@@ -184,32 +219,37 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
))}
</div>
)}
<div
className={clsx(classes.RootCustomNode, regionClass, classes[STATUS_CLASSES[status]], {
[classes.selected]: selected,
})}
className={clsx(
classes.RootCustomNode,
regionClass,
classes[STATUS_CLASSES[status]],
{ [classes.selected]: selected },
'flex flex-col w-[130px] h-[34px]',
'px-[6px] pt-[2px] pb-[3px] text-[10px]',
'leading-[1] space-y-[1px]',
'shadow-[0_0_5px_rgba(45,45,45,0.5)]',
'border border-[var(--pastel-blue-darken10)] rounded-[5px]',
)}
>
{visible && (
<>
<div className={classes.HeadRow}>
<div className={clsx(classes.HeadRow, 'flex items-center gap-[3px]')}>
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
{class_title ?? '-'}
</div>
{tag != null && tag !== '' && (
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div>
)}
<div
className={clsx(
classes.classSystemName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
'flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
)}
>
{solar_system_name}
{systemName}
</div>
{isWormhole && (
<div className={classes.statics}>
<div className={clsx(classes.statics, 'flex gap-[2px] text-[8px]')}>
{sortedStatics.map(x => (
<WormholeClassComp key={x} id={x} />
))}
@@ -221,40 +261,34 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
{customName}
</div>
)}
{!isWormhole && !customName && (
<div className={clsx(classes.BottomRow, 'flex items-center gap-[3px]')}>
<div className="flex items-center gap-2">
{tag != null && tag !== '' && (
<div className={clsx(classes.TagTitle, 'font-medium')}>{`[${tag}]`}</div>
)}
<div
className={clsx(
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
className={clsx(classes.customName)}
title={`${customName ?? ''} ${labelCustom ?? ''}`}
>
{region_name}
{customName} {labelCustom}
</div>
)}
</div>
{isWormhole && !customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }}></i>}
{hubs.includes(solar_system_id.toString()) && (
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }}></i>
)}
{charactersInSystem.length > 0 && (
<div className={clsx(classes.localCounter, { ['text-amber-300']: hasUserCharacters })}>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }}></i>
<span className="font-sans">{charactersInSystem.length}</span>
</div>
)}
</div>
<div className="flex items-center ml-auto gap-[2px]">
{locked && <i className={clsx(PrimeIcons.LOCK, 'text-[0.45rem] font-bold')} />}
{hubs.includes(solar_system_id.toString()) && (
<i className={clsx(PrimeIcons.MAP_MARKER, 'text-[0.45rem] font-bold')} />
)}
{charactersInSystem.length > 0 && (
<div
className={clsx(
classes.localCounter,
'flex gap-[2px]'
)}
>
<span className="font-sans text-[0.65rem]">{charactersInSystem.length}</span>
</div>
)}
</div>
</div>
</>
@@ -268,9 +302,8 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
))}
</div>
)}
{visible && isShowUnsplashedSignatures && (
<div className={clsx([classes.Unsplashed, classes['Unsplashed--right']])}>
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{unsplashedRight.map(x => (
<UnsplashedSignature key={x.sig_id} signature={x} />
))}
@@ -278,13 +311,33 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
)}
<div onMouseDownCapture={dbClick} className={classes.Handlers}>
<Handle
type="target"
position={Position.Bottom}
style={{
width: '100%',
height: '100%',
background: 'none',
cursor: 'cell',
pointerEvents: dropHandler,
opacity: 0,
borderRadius: 0 }}
id="whole-node-target"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{
width: '100%',
height: '55%',
background: 'none',
cursor: 'cell',
opacity: 0,
borderRadius: 0,
visibility: showHandlers ? 'visible' : 'hidden',}}
position={Position.Top}
id="a"
/>
@@ -294,7 +347,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: showHandlers ? 'visible' : 'hidden', cursor: 'cell', zIndex: 10 }}
position={Position.Right}
id="b"
/>
@@ -304,7 +357,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: 'hidden', cursor: 'cell' }}
position={Position.Bottom}
id="c"
/>
@@ -314,7 +367,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
[classes.selected]: selected,
[classes.Tick]: isThickConnections,
})}
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
style={{ visibility: showHandlers ? 'visible' : 'hidden', cursor: 'cell', zIndex: 10 }}
position={Position.Left}
id="d"
/>

View File

@@ -80,8 +80,8 @@ $tooltip-bg: #202020; // Dark background for tooltips
}
}
&.selected {
border-color: $pastel-pink;
.selected {
border-color: var($pastel-pink, #d291bc);
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -93,43 +93,55 @@ $tooltip-bg: #202020; // Dark background for tooltips
border: 1px solid $pastel-pink;
}
&.eve-system-status-home {
border: 1px solid darken($eve-solar-system-status-color-home, 30%);
background-image: linear-gradient(275deg, $eve-solar-system-status-friendly, transparent);
.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-friendly),
transparent
);
&.selected {
border-color: $eve-solar-system-status-color-home;
border-color: var(--eve-solar-system-status-color-home);
}
}
&.eve-system-status-friendly {
border: 1px solid darken($eve-solar-system-status-color-friendly, 20%);
background-image: linear-gradient(275deg, darken($eve-solar-system-status-friendly, 30%), transparent);
.eve-system-status-friendly {
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-friendly-dark30),
transparent
);
&.selected {
border-color: darken($eve-solar-system-status-color-friendly, 5%);
border-color: var(--eve-solar-system-status-color-friendly-dark5);
}
}
&.eve-system-status-lookingFor {
border: 1px solid darken($eve-solar-system-status-color-lookingFor, 15%);
.eve-system-status-lookingFor {
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: $pastel-pink;
border-color: var(--pastel-pink, #d291bc);
}
}
&.eve-system-status-warning {
background-image: linear-gradient(275deg, $eve-solar-system-status-warning, transparent);
.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, $eve-solar-system-status-dangerous, 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, $eve-solar-system-status-target, transparent);
.eve-system-status-target {
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-target),
transparent
);
}
}
@@ -242,10 +254,10 @@ $tooltip-bg: #202020; // Dark background for tooltips
.TagTitle {
font-size: 11px;
font-weight: bold;
font-weight: medium;
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
color: #ffb01d;
color: var(--rf-tag-color, #38BDF8);
}
/* Firefox kostyl */

View File

@@ -0,0 +1,235 @@
import { memo } from 'react';
import { Handle, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeDefault.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import {
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
EFFECT_BACKGROUND_STYLES,
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
export const SolarSystemNodeDefault = memo((props) => {
const nodeVars = useSolarSystemNode(props);
return (
<>
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">
{nodeVars.labelCustom}
</span>
</div>
)}
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
{nodeVars.killsCount && (
<div
className={clsx(
classes.Bookmark,
MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!]
)}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
)}
{nodeVars.labelsInfo.map(x => (
<div
key={x.id}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}
>
{x.shortName}
</div>
))}
</div>
)}
<div
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{ [classes.selected]: nodeVars.selected },
)}
>
{nodeVars.visible && (
<>
<div className={classes.HeadRow}>
<div
className={clsx(
classes.classTitle,
nodeVars.classTitleColor,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
)}
>
{nodeVars.classTitle ?? '-'}
</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>
{nodeVars.tag}
</div>
)}
<div
className={clsx(
classes.classSystemName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
)}
>
{nodeVars.systemName}
</div>
{nodeVars.isWormhole && (
<div className={classes.statics}>
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
))}
</div>
)}
{nodeVars.effectName !== null && nodeVars.isWormhole && (
<div
className={clsx(
classes.effect,
EFFECT_BACKGROUND_STYLES[nodeVars.effectName],
)}
/>
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
{nodeVars.customName}
</div>
)}
{!nodeVars.isWormhole && !nodeVars.customName && (
<div
className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5"
>
{nodeVars.regionName}
</div>
)}
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{nodeVars.locked && (
<i
className={PrimeIcons.LOCK}
style={{ fontSize: '0.45rem', fontWeight: 'bold' }}
/>
)}
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i
className={PrimeIcons.MAP_MARKER}
style={{ fontSize: '0.45rem', fontWeight: 'bold' }}
/>
)}
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
['text-amber-300']: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">
{nodeVars.charactersInSystem.length}
</span>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
{nodeVars.visible && (
<>
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
</>
)}
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"
/>
</div>
</>
);
});

View File

@@ -0,0 +1,402 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a;
$pastel-pink: #d291bc;
$pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020; // Dark background for tooltips
.RootCustomNode {
display: flex;
width: 130px;
height: 34px;
flex-direction: column;
padding: 2px 6px;
font-size: 10px;
background-color: var(--rf-node-bg-color, #202020) !important;
color: var(--rf-text-color, #ffffff);
font-family: var(--rf-node-font-family, inherit) !important;
font-weight: var(--rf-node-font-weight, inherit) !important;
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid darken($pastel-blue, 10%);
border-radius: 5px;
position: relative;
z-index: 1;
overflow: hidden;
&.Mataria,
&.Amarria,
&.Gallente,
&.Caldaria {
&::before {
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 {
&::before {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
background-position-x: 1px;
background-position-y: -14px;
}
}
&.Caldaria {
&::before {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
background-position-x: 1px;
background-position-y: -10px;
}
}
&.Amarria {
&::before {
opacity: 0.45;
background-image: url('/images/amarr-180.png');
background-position-x: 0;
background-position-y: -13px;
}
}
&.Gallente {
&::before {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
background-position-x: 1px;
background-position-y: 0;
}
}
.selected {
border-color: var($pastel-pink, #d291bc);
box-shadow: 0 0 10px #9a1af1c2;
}
.tooltip {
background-color: $tooltip-bg;
color: $text-color;
padding: 5px 10px;
border-radius: 3px;
border: 1px solid $pastel-pink;
}
.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(
275deg,
var(--eve-solar-system-status-friendly),
transparent
);
&.selected {
border-color: var(--eve-solar-system-status-color-home);
}
}
.eve-system-status-friendly {
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
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 {
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
&.selected {
border-color: var(--pastel-pink, #d291bc);
}
}
.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
);
}
}
.Bookmarks {
position: absolute;
width: 100%;
z-index: 1;
display: flex;
left: 4px;
& > .Bookmark {
min-width: 13px;
height: 22px;
position: relative;
top: -13px;
border-radius: 5px;
color: #ffffff;
font-size: 8px;
text-align: center;
padding-top: 2px;
font-weight: bolder;
padding-left: 3px;
padding-right: 3px;
//background-color: #833ca4;
&:not(:first-child) {
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
}
}
.BookmarkWithIcon {
display: flex;
justify-content: center;
align-items: center;
margin-top: -2px;
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
padding-right: 2px;
& > .icon {
width: 8px;
height: 8px;
font-size: 8px;
}
& > .text {
margin-top: 1px;
font-size: 9px;
}
}
}
.Unsplashed {
position: absolute;
width: calc(50% - 4px);
z-index: -1;
display: flex;
flex-wrap: wrap;
gap: 2px;
left: 2px;
&--right {
left: calc(50% + 6px);
}
& > .Signature {
width: 13px;
height: 4px;
position: relative;
top: 3px;
border-radius: 5px;
color: #ffffff;
font-size: 8px;
text-align: center;
padding-top: 2px;
font-weight: bolder;
padding-left: 3px;
padding-right: 3px;
display: block;
background-color: #833ca4;
&:not(:first-child) {
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
}
}
}
.icon {
width: 8px;
height: 8px;
font-size: 8px;
}
.HeadRow {
display: flex;
align-items: center;
gap: 3px;
font-size: 11px;
line-height: 14px;
font-weight: 500;
position: relative;
top: 1px;
.classTitle {
font-size: 11px;
font-weight: bold;
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
}
.TagTitle {
font-size: 11px;
font-weight: medium;
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
color: var(--rf-tag-color, #38BDF8);
}
/* Firefox kostyl */
@-moz-document url-prefix() {
.classSystemName {
font-family: inherit !important;
font-weight: bold;
}
}
.classSystemName {
font-family: inherit !important;
font-weight: bold;
}
.solarSystemName {
}
}
.BottomRow {
display: flex;
justify-content: space-between;
align-items: center;
height: 19px;
.regionName {
color: var(--rf-region-name, #D6D3D1)
}
.customName {
color: var(--rf-custom-name, #93C5FD)
}
.localCounter {
display: flex;
//align-items: center;
gap: 2px;
.hasUserCharacters {
color: var(--rf-has-user-characters, #fbbf24);
}
& > i {
position: relative;
top: 1px;
}
& > span {
font-size: 9px;
line-height: 9px;
font-weight: 500;
//margin-top: 1px;
}
}
}
.effect {
width: 8px;
height: 8px;
margin-top: -2px;
box-sizing: border-box;
border-radius: 2px;
margin-left: 1px;
}
.statics {
display: flex;
gap: 2px;
font-size: 8px;
& > * {
line-height: 10px;
}
/* Firefox kostyl */
@-moz-document url-prefix() {
position: relative;
top: -1px;
}
}
.Handlers {
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid $pastel-blue;
width: 5px;
height: 5px;
&.selected {
border-color: $pastel-pink;
}
&.HandleTop {
top: -2px;
}
&.HandleRight {
right: -2px;
}
&.HandleBottom {
bottom: -2px;
}
&.HandleLeft {
left: -2px;
}
&.Tick {
width: 7px;
height: 7px;
&.HandleTop {
top: -3px;
}
&.HandleRight {
right: -3px;
}
&.HandleBottom {
bottom: -3px;
}
&.HandleLeft {
left: -3px;
}
}
}

View File

@@ -0,0 +1,236 @@
import { memo } from 'react';
import { Handle, Position } from 'reactflow';
import clsx from 'clsx';
import classes from './SolarSystemNodeTheme.module.scss';
import { PrimeIcons } from 'primereact/api';
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
import {
MARKER_BOOKMARK_BG_STYLES,
STATUS_CLASSES,
EFFECT_BACKGROUND_STYLES,
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
export const SolarSystemNodeTheme = memo((props) => {
const nodeVars = useSolarSystemNode(props);
return (
<>
{nodeVars.visible && (
<div className={classes.Bookmarks}>
{nodeVars.labelCustom !== '' && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">
{nodeVars.labelCustom}
</span>
</div>
)}
{nodeVars.isShattered && (
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
<span className={clsx('pi pi-chart-pie', classes.icon)} />
</div>
)}
{nodeVars.killsCount && (
<div
className={clsx(
classes.Bookmark,
MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!]
)}
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</div>
)}
{nodeVars.labelsInfo.map(x => (
<div
key={x.id}
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}
>
{x.shortName}
</div>
))}
</div>
)}
<div
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
classes[STATUS_CLASSES[nodeVars.status]],
{ [classes.selected]: nodeVars.selected },
)}
>
{nodeVars.visible && (
<>
<div className={classes.HeadRow}>
<div
className={clsx(
classes.classTitle,
nodeVars.classTitleColor,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
)}
>
{nodeVars.classTitle ?? '-'}
</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<div className={clsx(classes.TagTitle)}>
{nodeVars.tag}
</div>
)}
<div
className={clsx(
classes.classSystemName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap',
)}
>
{nodeVars.systemName}
</div>
{nodeVars.isWormhole && (
<div className={classes.statics}>
{nodeVars.sortedStatics.map(whClass => (
<WormholeClassComp key={whClass} id={whClass} />
))}
</div>
)}
{nodeVars.effectName !== null && nodeVars.isWormhole && (
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
)}
</div>
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{nodeVars.customName && (
<div
className={clsx(
classes.CustomName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{nodeVars.customName}
</div>
)}
{!nodeVars.isWormhole && !nodeVars.customName && (
<div
className={clsx(
classes.RegionName,
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
)}
>
{nodeVars.regionName}
</div>
)}
{nodeVars.isWormhole && !nodeVars.customName && <div />}
<div className="flex items-center justify-end">
<div className="flex gap-1 items-center">
{nodeVars.locked && (
<i
className={PrimeIcons.LOCK}
style={{ fontSize: '0.45rem', fontWeight: 'bold' }}
/>
)}
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
<i
className={PrimeIcons.MAP_MARKER}
style={{ fontSize: '0.45rem', fontWeight: 'bold' }}
/>
)}
{nodeVars.charactersInSystem.length > 0 && (
<div
className={clsx(classes.localCounter, {
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
})}
>
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
{nodeVars.visible && (
<>
{nodeVars.unsplashedLeft.length > 0 && (
<div className={classes.Unsplashed}>
{nodeVars.unsplashedLeft.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
{nodeVars.unsplashedRight.length > 0 && (
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
{nodeVars.unsplashedRight.map(sig => (
<UnsplashedSignature key={sig.sig_id} signature={sig} />
))}
</div>
)}
</>
)}
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleTop, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Top}
id="a"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleRight, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Right}
id="b"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleBottom, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Bottom}
id="c"
/>
<Handle
type="source"
className={clsx(classes.Handle, classes.HandleLeft, {
[classes.selected]: nodeVars.selected,
[classes.Tick]: nodeVars.isThickConnections,
})}
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
position={Position.Left}
id="d"
/>
</div>
</>
);
});

View File

@@ -1 +1,2 @@
export * from './SolarSystemNode';
export * from './SolarSystemNodeDefault';
export * from './SolarSystemNodeTheme';

View File

@@ -9,7 +9,7 @@
width: 13px;
height: 4px;
border-radius: 4px;
color: #ffffff;
color: var(--text-color);
font-size: 8px;
text-align: center;
font-weight: bolder;

View File

@@ -0,0 +1,44 @@
import { useEffect, useState } from 'react';
import { BackgroundVariant } from 'reactflow';
export function useBackgroundVars(themeName?: string) {
const [variant, setVariant] = useState<BackgroundVariant>(BackgroundVariant.Dots);
const [gap, setGap] = useState<number>(16);
const [size, setSize] = useState<number>(1);
const [color, setColor] = useState('#81818b');
useEffect(() => {
// match any element whose entire `class` attribute ends with "-theme"
let themeEl = document.querySelector('[class$="-theme"]');
// If none is found, fall back to the <html> element
if (!themeEl) {
themeEl = document.documentElement;
}
const style = getComputedStyle(themeEl as HTMLElement);
const rawVariant = style.getPropertyValue('--rf-bg-variant').replace(/['"]/g, '').trim().toLowerCase();
let finalVariant: BackgroundVariant = BackgroundVariant.Dots;
if (rawVariant === 'lines') {
finalVariant = BackgroundVariant.Lines;
} else if (rawVariant === 'cross') {
finalVariant = BackgroundVariant.Cross;
}
const cssVarGap = style.getPropertyValue('--rf-bg-gap');
const cssVarSize = style.getPropertyValue('--rf-bg-size');
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
const gapNum = parseInt(cssVarGap, 10) || 16;
const sizeNum = parseInt(cssVarSize, 10) || 1;
setVariant(finalVariant);
setGap(gapNum);
setSize(sizeNum);
setColor(cssColor);
}, [themeName]);
return { variant, gap, size, color };
}

View File

@@ -0,0 +1,171 @@
import { useMemo } from 'react';
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_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
import { OutCommand } from '@/hooks/Mapper/types';
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
function getActivityType(count: number) {
if (count <= 5) return 'activityNormal';
if (count <= 30) return 'activityWarn';
return 'activityDanger';
}
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
};
function sortedLabels(labels: string[]) {
if (!labels) return [];
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
}
export function useSolarSystemNode(props: any) {
const { data, selected, id } = props;
const { system_static_info, system_signatures, locked, name, tag, status, labels, temporary_name } = data;
const {
system_class,
security,
class_title,
solar_system_id,
statics,
effect_name,
region_name,
region_id,
is_shattered,
solar_system_name,
} = system_static_info;
// Global map state
const { interfaceSettings } = useMapRootState();
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const {
data: {
characters,
presentCharacters,
wormholesData,
hubs,
kills,
userCharacters,
isConnecting,
hoverNodeId,
visibleNodes,
showKSpaceBG,
isThickConnections,
},
outCommand,
} = useMapState();
// logic
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
const charactersInSystem = useMemo(() => {
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
// eslint-disable-next-line
}, [characters, presentCharacters, solar_system_id]);
const isWormhole = isWormholeSpace(system_class);
const classTitleColor = useMemo(
() => getSystemClassStyles({ systemClass: system_class, security }),
[security, system_class],
);
const sortedStatics = useMemo(() => sortWHClasses(wormholesData, statics), [wormholesData, statics]);
const labelsManager = useMemo(() => new LabelsManager(labels ?? ''), [labels]);
const labelsInfo = useMemo(() => sortedLabels(labelsManager.list), [labelsManager]);
const labelCustom = useMemo(() => labelsManager.customLabel, [labelsManager]);
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
const hasUserCharacters = useMemo(() => {
return charactersInSystem.some(x => userCharacters.includes(x.eve_id));
}, [charactersInSystem, userCharacters]);
const dbClick = useDoubleClick(() => {
outCommand({
type: OutCommand.openSettings,
data: { system_id: solar_system_id.toString() },
});
});
const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const systemName = (isTempSystemNameEnabled && temporary_name) || solar_system_name;
const customName =
(isTempSystemNameEnabled && temporary_name && name) || (solar_system_name !== name && name);
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
}
return prepareUnsplashedChunks(
system_signatures
.filter(s => s.group === 'Wormhole' && !s.linked_system)
.map(s => ({
eve_id: s.eve_id,
type: s.type,
custom_info: s.custom_info,
})),
);
}, [isShowUnsplashedSignatures, system_signatures]);
const nodeVars = {
// original props
id,
selected,
// computed
visible,
isWormhole,
classTitleColor,
killsCount,
killsActivityType,
hasUserCharacters,
showHandlers,
regionClass,
systemName,
customName,
labelCustom,
isShattered: is_shattered,
tag,
status,
labelsInfo,
dbClick,
sortedStatics,
effectName: effect_name,
regionName: region_name,
solarSystemId: solar_system_id,
solarSystemName: solar_system_name,
locked,
hubs,
name: name,
isConnecting,
hoverNodeId,
charactersInSystem,
unsplashedLeft,
unsplashedRight,
isThickConnections,
classTitle: class_title,
temporaryName: temporary_name,
};
return nodeVars;
}

View File

@@ -1,5 +1,6 @@
import { SolarSystemRawType } from '@/hooks/Mapper/types/system';
import { SolarSystemConnection } from '@/hooks/Mapper/types';
import { XYPosition } from 'reactflow';
export type MapSolarSystemType = Omit<SolarSystemRawType, 'position'>;
@@ -7,3 +8,5 @@ export type OnMapSelectionChange = (event: {
systems: string[];
connections: Pick<SolarSystemConnection, 'source' | 'target'>[];
}) => void;
export type OnMapAddSystemCallback = (props: { coordinates: XYPosition | null }) => void;

View File

@@ -0,0 +1,31 @@
@import './eve-common-variables';
@import './eve-common';
.default-theme {
--rf-bg-color: #000000;
--rf-soft-bg-color: #171717;
--rf-node-bg-color: #202020;
--rf-node-soft-bg-color: #2b2b2b;
--rf-text-color: #ffffff;
--rf-tag-color: #38BDF8;
--rf-region-name: #D6D3D1;
--rf-custom-name: #93C5FD;
--rf-bg-variant: "dots";
--rf-bg-gap: 16;
--rf-bg-size: 1;
--rf-bg-pattern-color: #81818a;
--pastel-blue: #5a7d9a;
--pastel-pink: #d291bc;
--pastel-green: #88b04b;
--pastel-yellow: #ffdd59;
--dark-bg: #2d2d2d;
--text-color: #ffffff;
--tooltip-bg: #202020;
}

View File

@@ -1,78 +1,114 @@
$eve-link-color-default: #333;
$eve-link-color-top-mass-0: #333;
$eve-link-color-top-mass-1: #5a4520;
$eve-link-color-top-mass-2: #672c2c;
$eve-link-color-middle-mass-0: #333;
$eve-link-color-middle-mass-1: #333;
$eve-link-color-middle-mass-2: #333;
$eve-link-color-middle-time-0: #5c5c5c;
$eve-link-color-middle-time-1: #ff00cd;
$eve-link-color-middle-time-1-border: #99f3ff;
:root {
--pastel-blue: #5a7d9a;
--pastel-pink: #d291bc;
--pastel-green: #88b04b;
--pastel-yellow: #ffdd59;
--dark-bg: #2d2d2d;
--text-color: #ffffff;
--tooltip-bg: #202020;
$eve-link-color-top-mass-1-time-1: #796300;
$eve-link-color-top-mass-2-time-1: #8c1717;
$eve-link-color-temp: orange;
--pastel-blue-darken10: #4f6b86;
--pastel-blue-lighten10: #6da3af;
--pastel-pink-darken10: #bb7ca9;
--pastel-pink-lighten10: #e0a6cb;
$eve-effect-pulsar: #40aef5;
$eve-effect-magnetar: #f058f8;
$eve-effect-wolfRayet: #ef7843;
$eve-effect-blackHole: #1b1b1b;
$eve-effect-cataclysmicVariable: #ffea90;
$eve-effect-redGiant: #fd3c3c;
$eve-effect-dazhLiminalityLocus: #ff6464;
$eve-effect-imperialStellarObservatory: #6991ce;
$eve-effect-stateStellarObservatory: #6991ce;
$eve-effect-republicStellarObservatory: #6991ce;
$eve-effect-federalStellarObservatory: #6991ce;
--pastel-green-darken10: #79a244;
--pastel-green-lighten10: #99cf52;
$eve-wh-type-color-high: #5dffd2;
$eve-wh-type-color-low: #f79400;
$eve-wh-type-color-null: #fc3c3c;
$eve-wh-type-color-c1: #69bfce;
$eve-wh-type-color-c2: #6991ce;
$eve-wh-type-color-c3: #a8cb70;
$eve-wh-type-color-c4: #e39c68;
$eve-wh-type-color-c5: #de8686;
$eve-wh-type-color-c6: #e76363;
$eve-wh-type-color-c13: #988cb5;
$eve-wh-type-color-drifter: #ff44f6;
$eve-wh-type-color-thera: #ffffff;
$eve-wh-type-color-zarzakh: #212121;
--pastel-yellow-darken10: #e6c44f;
--pastel-yellow-lighten10: #ffe874;
$eve-security-color-10: #2c74df;
$eve-security-color-09: #3998e8;
$eve-security-color-08: #4dcbf5;
$eve-security-color-07: #60d8a2;
$eve-security-color-06: #71e454;
$eve-security-color-05: #f2fc81;
$eve-security-color-04: #d96c07;
$eve-security-color-03: #cb440f;
$eve-security-color-02: #b91117;
$eve-security-color-01: #732020;
$eve-security-color-00: #8b3263;
$eve-security-color-m-01: #8b3263;
$eve-security-color-m-02: #8b3263;
$eve-security-color-m-03: #8b3263;
$eve-security-color-m-04: #8b3263;
$eve-security-color-m-05: #8b3263;
$eve-security-color-m-06: #8b3263;
$eve-security-color-m-07: #8b3263;
$eve-security-color-m-08: #8b3263;
$eve-security-color-m-09: #8b3263;
$eve-security-color-m-10: #8b3263;
/* Eve Link Colors */
--eve-link-color-default: #333;
--eve-link-color-top-mass-0: #333;
--eve-link-color-top-mass-1: #5a4520;
--eve-link-color-top-mass-2: #672c2c;
--eve-link-color-middle-mass-0: #333;
--eve-link-color-middle-mass-1: #333;
--eve-link-color-middle-mass-2: #333;
--eve-link-color-middle-time-0: #5c5c5c;
--eve-link-color-middle-time-1: #ff00cd;
--eve-link-color-middle-time-1-border: #99f3ff;
--eve-link-color-top-mass-1-time-1: #796300;
--eve-link-color-top-mass-2-time-1: #8c1717;
--eve-link-color-temp: orange;
$eve-solar-system-status-unknown: transparent;
$eve-solar-system-status-friendly: #3bbd3952;
$eve-solar-system-status-warning: #906518a6;
$eve-solar-system-status-target: #b439ff6b;
$eve-solar-system-status-dangerous: #d54040;
$eve-solar-system-status-lookingFor: rgba(67, 176, 253, 0.48);
$eve-solar-system-status-home: rgb(197, 253, 67);
/* Wormhole Effects */
--eve-effect-pulsar: #40aef5;
--eve-effect-magnetar: #f058f8;
--eve-effect-wolfRayet: #ef7843;
--eve-effect-blackHole: #1b1b1b;
--eve-effect-cataclysmicVariable: #ffea90;
--eve-effect-redGiant: #fd3c3c;
--eve-effect-dazhLiminalityLocus: #ff6464;
--eve-effect-imperialStellarObservatory: #6991ce;
--eve-effect-stateStellarObservatory: #6991ce;
--eve-effect-republicStellarObservatory: #6991ce;
--eve-effect-federalStellarObservatory: #6991ce;
$eve-solar-system-status-color-unknown: transparent;
$eve-solar-system-status-color-friendly: #3bbd39;
$eve-solar-system-status-color-warning: #ffb93b;
$eve-solar-system-status-color-target: #b439ff;
$eve-solar-system-status-color-dangerous: #d54040;
$eve-solar-system-status-color-lookingFor: #43c2fd;
$eve-solar-system-status-color-home: rgb(197, 253, 67);
/* WH Types */
--eve-wh-type-color-high: #5dffd2;
--eve-wh-type-color-low: #f79400;
--eve-wh-type-color-null: #fc3c3c;
--eve-wh-type-color-c1: #69bfce;
--eve-wh-type-color-c2: #6991ce;
--eve-wh-type-color-c3: #a8cb70;
--eve-wh-type-color-c4: #e39c68;
--eve-wh-type-color-c5: #de8686;
--eve-wh-type-color-c6: #e76363;
--eve-wh-type-color-c13: #988cb5;
--eve-wh-type-color-drifter: #ff44f6;
--eve-wh-type-color-thera: #ffffff;
--eve-wh-type-color-zarzakh: #212121;
/* Security Colors */
--eve-security-color-10: #2c74df;
--eve-security-color-09: #3998e8;
--eve-security-color-08: #4dcbf5;
--eve-security-color-07: #60d8a2;
--eve-security-color-06: #71e454;
--eve-security-color-05: #f2fc81;
--eve-security-color-04: #d96c07;
--eve-security-color-03: #cb440f;
--eve-security-color-02: #b91117;
--eve-security-color-01: #732020;
--eve-security-color-00: #8b3263;
--eve-security-color-m-01: #8b3263;
--eve-security-color-m-02: #8b3263;
--eve-security-color-m-03: #8b3263;
--eve-security-color-m-04: #8b3263;
--eve-security-color-m-05: #8b3263;
--eve-security-color-m-06: #8b3263;
--eve-security-color-m-07: #8b3263;
--eve-security-color-m-08: #8b3263;
--eve-security-color-m-09: #8b3263;
--eve-security-color-m-10: #8b3263;
/* Solar System Status */
--eve-solar-system-status-unknown: transparent;
--eve-solar-system-status-friendly: #3bbd3952;
--eve-solar-system-status-warning: #906518a6;
--eve-solar-system-status-target: #b439ff6b;
--eve-solar-system-status-dangerous: #d54040;
--eve-solar-system-status-lookingFor: rgba(67, 176, 253, 0.48);
--eve-solar-system-status-home: rgb(197, 253, 67);
--eve-solar-system-status-color-unknown: transparent;
--eve-solar-system-status-color-friendly: #3bbd39;
--eve-solar-system-status-color-warning: #ffb93b;
--eve-solar-system-status-color-target: #b439ff;
--eve-solar-system-status-color-dangerous: #d54040;
--eve-solar-system-status-color-lookingFor: #43c2fd;
--eve-solar-system-status-color-home: rgb(197, 253, 67);
--eve-solar-system-status-color-friendly-dark20: #2d9b2e;
--eve-solar-system-status-friendly-dark30: #28892a;
--eve-solar-system-status-color-friendly-dark5: #38b538;
--eve-solar-system-status-color-lookingFor-dark15: #32aadf;
/* Context Menu */
--conn-time-eol: #7452c3e3;
--conn-frigate: #325d88;
--conn-save: rgba(155, 102, 45, 0.85);
--selected-item-bg: rgba(98, 98, 98, 0.33);
}

View File

@@ -1,535 +1,448 @@
@import "eve-common-variables";
@import './eve-common-variables';
.eve-wh-effect-color-pulsar {
fill: $eve-effect-pulsar;
background-color: $eve-effect-pulsar;
fill: var(--eve-effect-pulsar);
background-color: var(--eve-effect-pulsar);
}
.eve-wh-effect-color-magnetar {
fill: $eve-effect-magnetar;
background-color: $eve-effect-magnetar;
fill: var(--eve-effect-magnetar);
background-color: var(--eve-effect-magnetar);
}
.eve-wh-effect-color-wolfRayet {
fill: $eve-effect-wolfRayet;
background-color: $eve-effect-wolfRayet;
fill: var(--eve-effect-wolfRayet);
background-color: var(--eve-effect-wolfRayet);
}
.eve-wh-effect-color-blackHole {
fill: $eve-effect-blackHole;
background-color: $eve-effect-blackHole;
box-shadow: 0 0 8px rgba(255 255 255 / 33);
fill: var(--eve-effect-blackHole);
background-color: var(--eve-effect-blackHole);
box-shadow: 0 0 8px rgba(255, 255, 255, 0.33);
}
.eve-wh-effect-color-cataclysmicVariable {
fill: $eve-effect-cataclysmicVariable;
background-color: $eve-effect-cataclysmicVariable;
fill: var(--eve-effect-cataclysmicVariable);
background-color: var(--eve-effect-cataclysmicVariable);
}
.eve-wh-effect-color-redGiant {
fill: $eve-effect-redGiant;
background-color: $eve-effect-redGiant;
fill: var(--eve-effect-redGiant);
background-color: var(--eve-effect-redGiant);
}
.text-eve-wh-effect-color-pulsar {
color: $eve-effect-pulsar;
color: var(--eve-effect-pulsar);
}
.text-eve-wh-effect-color-magnetar {
color: $eve-effect-magnetar;
color: var(--eve-effect-magnetar);
}
.text-eve-wh-effect-color-wolfRayet {
color: $eve-effect-wolfRayet;
color: var(--eve-effect-wolfRayet);
}
.text-eve-wh-effect-color-blackHole {
color: #fff;
}
.text-eve-wh-effect-color-cataclysmicVariable {
color: $eve-effect-cataclysmicVariable;
color: var(--eve-effect-cataclysmicVariable);
}
.text-eve-wh-effect-color-redGiant {
color: $eve-effect-redGiant;
color: var(--eve-effect-redGiant);
}
.text-eve-wh-effect-color-dazhLiminalityLocus {
color: $eve-effect-dazhLiminalityLocus;
color: var(--eve-effect-dazhLiminalityLocus);
}
.text-eve-wh-effect-color-imperialStellarObservatory {
color: $eve-effect-imperialStellarObservatory;
color: var(--eve-effect-imperialStellarObservatory);
}
.text-eve-wh-effect-color-stateStellarObservatory {
color: $eve-effect-stateStellarObservatory;
color: var(--eve-effect-stateStellarObservatory);
}
.text-eve-wh-effect-color-republicStellarObservatory {
color: $eve-effect-republicStellarObservatory;
color: var(--eve-effect-republicStellarObservatory);
}
.text-eve-wh-effect-color-federalStellarObservatory {
color: $eve-effect-federalStellarObservatory;
color: var(--eve-effect-federalStellarObservatory);
}
/* Security color classes */
.eve-security-color-10 {
color: $eve-security-color-10 !important;
fill: $eve-security-color-10;
color: var(--eve-security-color-10) !important;
fill: var(--eve-security-color-10);
}
.eve-security-color-09 {
color: $eve-security-color-09 !important;
fill: $eve-security-color-09;
color: var(--eve-security-color-09) !important;
fill: var(--eve-security-color-09);
}
.eve-security-color-08 {
color: $eve-security-color-08 !important;
fill: $eve-security-color-08;
color: var(--eve-security-color-08) !important;
fill: var(--eve-security-color-08);
}
.eve-security-color-07 {
color: $eve-security-color-07 !important;
fill: $eve-security-color-07;
color: var(--eve-security-color-07) !important;
fill: var(--eve-security-color-07);
}
.eve-security-color-06 {
color: $eve-security-color-06 !important;
fill: $eve-security-color-06;
color: var(--eve-security-color-06) !important;
fill: var(--eve-security-color-06);
}
.eve-security-color-05 {
color: $eve-security-color-05 !important;
fill: $eve-security-color-05;
color: var(--eve-security-color-05) !important;
fill: var(--eve-security-color-05);
}
.eve-security-color-04 {
color: $eve-security-color-04 !important;
fill: $eve-security-color-04;
color: var(--eve-security-color-04) !important;
fill: var(--eve-security-color-04);
}
.eve-security-color-03 {
color: $eve-security-color-03 !important;
fill: $eve-security-color-03;
color: var(--eve-security-color-03) !important;
fill: var(--eve-security-color-03);
}
.eve-security-color-02 {
color: $eve-security-color-02 !important;
fill: $eve-security-color-02;
color: var(--eve-security-color-02) !important;
fill: var(--eve-security-color-02);
}
.eve-security-color-01 {
color: $eve-security-color-01 !important;
fill: $eve-security-color-01;
color: var(--eve-security-color-01) !important;
fill: var(--eve-security-color-01);
}
.eve-security-color-00 {
color: $eve-security-color-00 !important;
fill: $eve-security-color-00;
color: var(--eve-security-color-00) !important;
fill: var(--eve-security-color-00);
}
.eve-security-color-m-01 {
color: $eve-security-color-m-01 !important;
fill: $eve-security-color-m-01;
color: var(--eve-security-color-m-01) !important;
fill: var(--eve-security-color-m-01);
}
.eve-security-color-m-02 {
color: $eve-security-color-m-02 !important;
fill: $eve-security-color-m-02;
color: var(--eve-security-color-m-02) !important;
fill: var(--eve-security-color-m-02);
}
.eve-security-color-m-03 {
color: $eve-security-color-m-03 !important;
fill: $eve-security-color-m-03;
color: var(--eve-security-color-m-03) !important;
fill: var(--eve-security-color-m-03);
}
.eve-security-color-m-04 {
color: $eve-security-color-m-04 !important;
fill: $eve-security-color-m-04;
color: var(--eve-security-color-m-04) !important;
fill: var(--eve-security-color-m-04);
}
.eve-security-color-m-05 {
color: $eve-security-color-m-05 !important;
fill: $eve-security-color-m-05;
color: var(--eve-security-color-m-05) !important;
fill: var(--eve-security-color-m-05);
}
.eve-security-color-m-06 {
color: $eve-security-color-m-06 !important;
fill: $eve-security-color-m-06;
color: var(--eve-security-color-m-06) !important;
fill: var(--eve-security-color-m-06);
}
.eve-security-color-m-07 {
color: $eve-security-color-m-07 !important;
fill: $eve-security-color-m-07;
color: var(--eve-security-color-m-07) !important;
fill: var(--eve-security-color-m-07);
}
.eve-security-color-m-08 {
color: $eve-security-color-m-08 !important;
fill: $eve-security-color-m-08;
color: var(--eve-security-color-m-08) !important;
fill: var(--eve-security-color-m-08);
}
.eve-security-color-m-09 {
color: $eve-security-color-m-09 !important;
fill: $eve-security-color-m-09;
color: var(--eve-security-color-m-09) !important;
fill: var(--eve-security-color-m-09);
}
.eve-security-color-m-10 {
color: $eve-security-color-m-10 !important;
fill: $eve-security-color-m-10;
color: var(--eve-security-color-m-10) !important;
fill: var(--eve-security-color-m-10);
}
/* Security backgrounds */
.eve-security-background-10 {
background-color: $eve-security-color-10;
fill: $eve-security-color-10;
background-color: var(--eve-security-color-10);
fill: var(--eve-security-color-10);
}
.eve-security-background-09 {
background-color: $eve-security-color-09;
fill: $eve-security-color-09;
background-color: var(--eve-security-color-09);
fill: var(--eve-security-color-09);
}
.eve-security-background-08 {
background-color: $eve-security-color-08;
fill: $eve-security-color-08;
background-color: var(--eve-security-color-08);
fill: var(--eve-security-color-08);
}
.eve-security-background-07 {
background-color: $eve-security-color-07;
fill: $eve-security-color-07;
background-color: var(--eve-security-color-07);
fill: var(--eve-security-color-07);
}
.eve-security-background-06 {
background-color: $eve-security-color-06;
fill: $eve-security-color-06;
background-color: var(--eve-security-color-06);
fill: var(--eve-security-color-06);
}
.eve-security-background-05 {
background-color: $eve-security-color-05;
fill: $eve-security-color-05;
background-color: var(--eve-security-color-05);
fill: var(--eve-security-color-05);
}
.eve-security-background-04 {
background-color: $eve-security-color-04;
fill: $eve-security-color-04;
background-color: var(--eve-security-color-04);
fill: var(--eve-security-color-04);
}
.eve-security-background-03 {
background-color: $eve-security-color-03;
fill: $eve-security-color-03;
background-color: var(--eve-security-color-03);
fill: var(--eve-security-color-03);
}
.eve-security-background-02 {
background-color: $eve-security-color-02;
fill: $eve-security-color-02;
background-color: var(--eve-security-color-02);
fill: var(--eve-security-color-02);
}
.eve-security-background-01 {
background-color: $eve-security-color-01;
fill: $eve-security-color-01;
background-color: var(--eve-security-color-01);
fill: var(--eve-security-color-01);
}
.eve-security-background-00 {
background-color: $eve-security-color-00;
fill: $eve-security-color-00;
background-color: var(--eve-security-color-00);
fill: var(--eve-security-color-00);
}
.eve-security-background-m-01 {
background-color: $eve-security-color-m-01;
fill: $eve-security-color-m-01;
background-color: var(--eve-security-color-m-01);
fill: var(--eve-security-color-m-01);
}
.eve-security-background-m-02 {
background-color: $eve-security-color-m-02;
fill: $eve-security-color-m-02;
background-color: var(--eve-security-color-m-02);
fill: var(--eve-security-color-m-02);
}
.eve-security-background-m-03 {
background-color: $eve-security-color-m-03;
fill: $eve-security-color-m-03;
background-color: var(--eve-security-color-m-03);
fill: var(--eve-security-color-m-03);
}
.eve-security-background-m-04 {
background-color: $eve-security-color-m-04;
fill: $eve-security-color-m-04;
background-color: var(--eve-security-color-m-04);
fill: var(--eve-security-color-m-04);
}
.eve-security-background-m-05 {
background-color: $eve-security-color-m-05;
fill: $eve-security-color-m-05;
background-color: var(--eve-security-color-m-05);
fill: var(--eve-security-color-m-05);
}
.eve-security-background-m-06 {
background-color: $eve-security-color-m-06;
fill: $eve-security-color-m-06;
background-color: var(--eve-security-color-m-06);
fill: var(--eve-security-color-m-06);
}
.eve-security-background-m-07 {
background-color: $eve-security-color-m-07;
fill: $eve-security-color-m-07;
background-color: var(--eve-security-color-m-07);
fill: var(--eve-security-color-m-07);
}
.eve-security-background-m-08 {
background-color: $eve-security-color-m-08;
fill: $eve-security-color-m-08;
background-color: var(--eve-security-color-m-08);
fill: var(--eve-security-color-m-08);
}
.eve-security-background-m-09 {
background-color: $eve-security-color-m-09;
fill: $eve-security-color-m-09;
background-color: var(--eve-security-color-m-09);
fill: var(--eve-security-color-m-09);
}
.eve-security-background-m-10 {
background-color: $eve-security-color-m-10;
fill: $eve-security-color-m-10;
background-color: var(--eve-security-color-m-10);
fill: var(--eve-security-color-m-10);
}
/* WH Type color classes */
.eve-wh-type-color-high {
color: $eve-wh-type-color-high;
fill: $eve-wh-type-color-high;
color: var(--eve-wh-type-color-high) !important;
fill: var(--eve-wh-type-color-high);
font-weight: bold !important;
}
.eve-wh-type-color-low {
color: $eve-wh-type-color-low;
fill: $eve-wh-type-color-low;
color: var(--eve-wh-type-color-low) !important;
fill: var(--eve-wh-type-color-low);
font-weight: bold !important;
}
.eve-wh-type-color-null {
color: $eve-wh-type-color-null;
fill: $eve-wh-type-color-null;
color: var(--eve-wh-type-color-null) !important;
fill: var(--eve-wh-type-color-null);
font-weight: bold !important;
}
.eve-wh-type-color-c1 {
color: $eve-wh-type-color-c1 !important;
fill: $eve-wh-type-color-c1;
color: var(--eve-wh-type-color-c1) !important;
fill: var(--eve-wh-type-color-c1);
font-weight: bold !important;
}
.eve-wh-type-color-c2 {
color: $eve-wh-type-color-c2 !important;
fill: $eve-wh-type-color-c2;
color: var(--eve-wh-type-color-c2) !important;
fill: var(--eve-wh-type-color-c2);
font-weight: bold !important;
}
.eve-wh-type-color-c3 {
color: $eve-wh-type-color-c3 !important;
fill: $eve-wh-type-color-c3;
color: var(--eve-wh-type-color-c3) !important;
fill: var(--eve-wh-type-color-c3);
font-weight: bold !important;
}
.eve-wh-type-color-c4 {
color: $eve-wh-type-color-c4 !important;
fill: $eve-wh-type-color-c4;
color: var(--eve-wh-type-color-c4) !important;
fill: var(--eve-wh-type-color-c4);
font-weight: bold !important;
}
.eve-wh-type-color-c5 {
color: $eve-wh-type-color-c5 !important;
fill: $eve-wh-type-color-c5;
color: var(--eve-wh-type-color-c5) !important;
fill: var(--eve-wh-type-color-c5);
font-weight: bold !important;
}
.eve-wh-type-color-c6 {
color: $eve-wh-type-color-c6 !important;
fill: $eve-wh-type-color-c6;
color: var(--eve-wh-type-color-c6) !important;
fill: var(--eve-wh-type-color-c6);
font-weight: bold !important;
}
.eve-wh-type-color-c13 {
color: $eve-wh-type-color-c13 !important;
fill: $eve-wh-type-color-c13;
color: var(--eve-wh-type-color-c13) !important;
fill: var(--eve-wh-type-color-c13);
}
.eve-wh-type-color-drifter {
color: $eve-wh-type-color-drifter !important;
fill: $eve-wh-type-color-drifter;
color: var(--eve-wh-type-color-drifter) !important;
fill: var(--eve-wh-type-color-drifter);
}
.eve-wh-type-color-thera {
color: $eve-wh-type-color-thera !important;
fill: $eve-wh-type-color-thera;
color: var(--eve-wh-type-color-thera) !important;
fill: var(--eve-wh-type-color-thera);
}
/* WH Type backgrounds */
.eve-wh-type-background-high {
background-color: $eve-wh-type-color-high;
background-color: var(--eve-wh-type-color-high);
}
.eve-wh-type-background-low {
background-color: $eve-wh-type-color-low;
background-color: var(--eve-wh-type-color-low);
}
.eve-wh-type-background-null {
background-color: $eve-wh-type-color-null;
background-color: var(--eve-wh-type-color-null);
}
.eve-wh-type-background-c1 {
background-color: $eve-wh-type-color-c1;
background-color: var(--eve-wh-type-color-c1);
}
.eve-wh-type-background-c2 {
background-color: $eve-wh-type-color-c2;
background-color: var(--eve-wh-type-color-c2);
}
.eve-wh-type-background-c3 {
background-color: $eve-wh-type-color-c3;
background-color: var(--eve-wh-type-color-c3);
}
.eve-wh-type-background-c4 {
background-color: $eve-wh-type-color-c4;
background-color: var(--eve-wh-type-color-c4);
}
.eve-wh-type-background-c5 {
background-color: $eve-wh-type-color-c5;
background-color: var(--eve-wh-type-color-c5);
}
.eve-wh-type-background-c6 {
background-color: $eve-wh-type-color-c6;
background-color: var(--eve-wh-type-color-c6);
}
.eve-wh-type-background-c13 {
background-color: $eve-wh-type-color-c13;
background-color: var(--eve-wh-type-color-c13);
}
.eve-wh-type-background-drifter {
background-color: $eve-wh-type-color-drifter;
background-color: var(--eve-wh-type-color-drifter);
}
.eve-wh-type-background-thera {
background-color: $eve-wh-type-color-thera;
background-color: var(--eve-wh-type-color-thera);
}
.eve-wh-type-background-zarzakh {
background-color: $eve-wh-type-color-zarzakh;
background-color: var(--eve-wh-type-color-zarzakh);
}
/* Kind color classes */
.eve-kind-color-high {
color: $eve-wh-type-color-high;
fill: $eve-wh-type-color-high;
color: var(--eve-wh-type-color-high);
fill: var(--eve-wh-type-color-high);
}
.eve-kind-color-low {
color: $eve-wh-type-color-low;
fill: $eve-wh-type-color-low;
color: var(--eve-wh-type-color-low);
fill: var(--eve-wh-type-color-low);
font-weight: bold;
}
.eve-kind-color-null {
color: $eve-wh-type-color-null;
fill: $eve-wh-type-color-null;
color: var(--eve-wh-type-color-null);
fill: var(--eve-wh-type-color-null);
}
.eve-kind-color-wh {
color: $eve-wh-type-color-c6;
fill: $eve-wh-type-color-c6;
color: var(--eve-wh-type-color-c6);
fill: var(--eve-wh-type-color-c6);
}
.eve-kind-color-thera {
color: $eve-wh-type-color-thera;
fill: $eve-wh-type-color-thera;
color: var(--eve-wh-type-color-thera);
fill: var(--eve-wh-type-color-thera);
}
.eve-kind-color-abyss {
color: $eve-wh-type-color-c6;
fill: $eve-wh-type-color-c6;
color: var(--eve-wh-type-color-c6);
fill: var(--eve-wh-type-color-c6);
}
.eve-kind-color-penalty {
color: $eve-wh-type-color-c6;
fill: $eve-wh-type-color-c6;
color: var(--eve-wh-type-color-c6);
fill: var(--eve-wh-type-color-c6);
}
.eve-kind-color-pochven {
color: $eve-wh-type-color-c6;
fill: $eve-wh-type-color-c6;
color: var(--eve-wh-type-color-c6);
fill: var(--eve-wh-type-color-c6);
}
.eve-kind-color-zarzakh {
color: $eve-wh-type-color-zarzakh;
fill: $eve-wh-type-color-zarzakh;
color: var(--eve-wh-type-color-zarzakh);
fill: var(--eve-wh-type-color-zarzakh);
}
/* Kind backgrounds */
.eve-kind-background-high {
background-color: $eve-wh-type-color-high;
background-color: var(--eve-wh-type-color-high);
}
.eve-kind-background-low {
background-color: $eve-wh-type-color-low;
background-color: var(--eve-wh-type-color-low);
}
.eve-kind-background-null {
background-color: $eve-wh-type-color-null;
background-color: var(--eve-wh-type-color-null);
}
.eve-kind-background-wh {
background-color: $eve-wh-type-color-c6;
background-color: var(--eve-wh-type-color-c6);
}
.eve-kind-background-thera {
background-color: $eve-wh-type-color-thera;
background-color: var(--eve-wh-type-color-thera);
}
.eve-kind-background-abyss {
background-color: $eve-wh-type-color-c6;
background-color: var(--eve-wh-type-color-c6);
}
.eve-kind-background-penalty {
background-color: $eve-wh-type-color-c6;
background-color: var(--eve-wh-type-color-c6);
}
.eve-kind-background-pochven {
background-color: $eve-wh-type-color-c6;
background-color: var(--eve-wh-type-color-c6);
}
.eve-kind-background-zarzakh {
background-color: $eve-wh-type-color-zarzakh;
background-color: var(--eve-wh-type-color-zarzakh);
}
/* System status color classes */
.eve-system-status-color-clear {
color: $eve-solar-system-status-color-unknown;
color: var(--eve-solar-system-status-color-unknown);
}
.eve-system-status-color-home {
color: $eve-solar-system-status-color-home;
color: var(--eve-solar-system-status-color-home);
}
.eve-system-status-color-friendly {
color: $eve-solar-system-status-color-friendly;
color: var(--eve-solar-system-status-color-friendly);
}
.eve-system-status-color-lookingFor {
color: $eve-solar-system-status-color-lookingFor;
color: var(--eve-solar-system-status-color-lookingFor);
}
.eve-system-status-color-warning {
color: $eve-solar-system-status-color-warning;
color: var(--eve-solar-system-status-color-warning);
}
.eve-system-status-color-target {
color: $eve-solar-system-status-color-target;
color: var(--eve-solar-system-status-color-target);
}
.eve-system-status-color-dangerous {
color: $eve-solar-system-status-color-dangerous;
color: var(--eve-solar-system-status-color-dangerous);
}
/* Shapes */
.wd-route-system-shape-triangle {
clip-path: polygon(50% 0, 0 100%, 100% 100%);
}
.wd-route-system-shape-circle {
border-radius: 40%;
}
/* Some additional background classes */
.wd-marker-bookmark-color-shattered {
background-color: #833ca4;
margin-top: 1px;
}
.wd-marker-bookmark-color-custom {
background-color: #282828;
border: 1px solid #4c4c4c;
@@ -572,3 +485,49 @@
.wd-marker-bookmark-color-danger {
background-color: #d10600;
}
.react-flow {
color: var(--text-color);
&__pane {
cursor: auto;
}
&__minimap {
background-color: rgba(66, 66, 66, 1);
opacity: 0.7;
border: 1px solid #2f2f2f;
border-radius: 4px;
overflow: hidden;
}
&__minimap-mask {
fill: rgba(28, 28, 28, 0.75);
}
&__controls {
filter: brightness(1.5);
}
&__minimap-node {
fill: #ffb03a;
}
}
.context-menu-active {
background-color: rgba(131, 131, 131, 0.33);
}
.p-dialog {
.p-dialog-header {
height: 40px;
padding: 1rem;
padding-right: 10px !important;
}
.p-dialog-title {
font-size: 1rem !important;
}
.p-dialog-header-icons {
align-self: initial !important;
}
}

View File

@@ -0,0 +1,2 @@
@import './default-theme.scss';
@import './pathfinder-theme.scss';

View File

@@ -1,74 +0,0 @@
$pastel-blue: #5a7d9a;
$pastel-pink: #d291bc;
$pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
.react-flow {
// background-color: $dark-bg;
color: $text-color;
&__node {
//cursor: auto;
}
&__pane {
cursor: auto;
}
//&__edge {
// stroke: $pastel-pink;
// stroke-width: 2px;
//
// &.selected {
// stroke: $pastel-yellow;
// }
//}
&__handle {
//background-color: $pastel-green;
//box-shadow: 0 0 5px rgba($pastel-green, 0.5);
}
&__minimap {
background-color: rgba(66, 66, 66, 1);
opacity: 0.7;
//backdrop-filter: blur(5px);
border: 1px solid #2f2f2f;
border-radius: 4px;
overflow: hidden;
}
&__minimap-mask {
fill: rgba(28, 28, 28, 0.75);
}
&__controls {
filter: brightness(1.5);
}
&__minimap-node {
fill: #ffb03a;
}
}
.context-menu-active {
background-color: rgba(131, 131, 131, 0.33);
}
.p-dialog {
.p-dialog-header {
height: 40px;
padding: 1rem;
padding-right: 10px !important;
}
.p-dialog-title {
font-size: 1rem !important;
}
.p-dialog-header-icons {
align-self: initial !important;
}
}

View File

@@ -1,7 +0,0 @@
$pastel-blue: #5a7d9a;
$pastel-pink: #d291bc;
$pastel-green: #88b04b;
$pastel-yellow: #ffdd59;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;

View File

@@ -0,0 +1,51 @@
@import './eve-common-variables';
@import './eve-common';
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
.pathfinder-theme {
--rf-bg-color: #000000;
--rf-soft-bg-color: #282828;
--rf-node-bg-color: #202020;
--rf-node-soft-bg-color: #313335;
--rf-node-font-weight: bold;
--rf-text-color: #adadad;
--rf-region-name: var(--rf-text-color);
--rf-custom-name: var(--rf-text-color);
--tooltip-bg: #202020;
--rf-bg-variant: "lines";
--rf-bg-gap: 32;
--rf-bg-size: 1;
--rf-bg-pattern-color: #313131;
--eve-effect-pulsar: #428bca;
--eve-effect-magnetar: #e06fdf;
--eve-effect-wolfRayet: #e28a0d;
--eve-effect-blackHole: #000000;
--eve-effect-cataclysmicVariable: #ffffbb;
--eve-effect-redGiant: #d9534f;
--eve-wh-type-color-high: #5cb85c;
--eve-wh-type-color-low: #e28a0d;
--eve-wh-type-color-null: #d9534f;
--eve-wh-type-color-c1: #428bca;
--eve-wh-type-color-c2: #428bca;
--eve-wh-type-color-c3: #e28a0d;
--eve-wh-type-color-c4: #e28a0d;
--eve-wh-type-color-c5: #d9534f;
--eve-wh-type-color-c6: #d9534f;
--eve-wh-type-color-c13: #7986cb;
--eve-wh-type-color-drifter: #44aa82;
--rf-node-font-weight: bold;
--rf-node-line-height: normal;
--rf-node-font-family: 'Oxygen', sans-serif;
--rf-node-text-color: var(--pf-text-color);
--rf-tag-color: #fbbf24;
--rf-has-user-characters: #5cb85c;
}

View File

@@ -0,0 +1,11 @@
// wrapNode.ts
import { NodeProps } from 'reactflow';
import { SolarSystemNodeProps } from '../components/SolarSystemNode';
export function wrapNode<T>(
SolarSystemNode: React.FC<SolarSystemNodeProps<T>>
): React.FC<NodeProps<T>> {
return function NodeAdapter(props) {
return <SolarSystemNode {...props} />;
};
}

View File

@@ -0,0 +1,10 @@
.SearchItem {
& > * {
font-size: 13px !important;
}
}
.SearchItemEffect {
font-weight: initial !important;
}

View File

@@ -0,0 +1,203 @@
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { IconField } from 'primereact/iconfield';
import { AutoComplete } from 'primereact/autocomplete';
import { OutCommand, SearchSystemItem } from '@/hooks/Mapper/types';
import { SystemViewStandalone, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
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';
export type SearchOnSubmitCallback = (item: SearchSystemItem) => void;
interface AddSystemDialogProps {
title?: string;
visible: boolean;
setVisible: (visible: boolean) => void;
onSubmit?: SearchOnSubmitCallback;
excludedSystems?: number[];
}
export const AddSystemDialog = ({
title = 'Add system',
visible,
setVisible,
onSubmit,
excludedSystems = [],
}: AddSystemDialogProps) => {
const {
outCommand,
data: { wormholesData },
} = useMapRootState();
const inputRef = useRef<any>();
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
const [filteredItems, setFilteredItems] = useState<SearchSystemItem[]>([]);
const [selectedItem, setSelectedItem] = useState<SearchSystemItem[] | null>(null);
const searchItems = useCallback(
async (event: { query: string }) => {
if (event.query.length < 2) {
setFilteredItems([]);
return;
}
const query = event.query;
if (query.length === 0) {
setFilteredItems([]);
} else {
try {
const result = await outCommand({
type: OutCommand.searchSystems,
data: {
text: query,
},
});
let prepared = (result.systems as SearchSystemItem[]).sort((a, b) => {
const amatch = a.label.indexOf(query);
const bmatch = b.label.indexOf(query);
return amatch - bmatch;
});
if (excludedSystems) {
prepared = prepared.filter(x => !excludedSystems.includes(x.system_static_info.solar_system_id));
}
setFilteredItems(prepared);
} catch (error) {
console.error('Error fetching data:', error);
setFilteredItems([]);
}
}
},
[excludedSystems, outCommand],
);
const ref = useRef({ onSubmit, selectedItem });
ref.current = { onSubmit, selectedItem };
const handleSubmit = useCallback(() => {
const { onSubmit, selectedItem } = ref.current;
setFilteredItems([]);
setSelectedItem([]);
if (!selectedItem) {
setVisible(false);
return;
}
onSubmit?.(selectedItem[0]);
setVisible(false);
}, [setVisible]);
return (
<Dialog
header={title}
visible={visible}
draggable={false}
style={{ width: '520px' }}
onShow={onShow}
onHide={() => {
if (!visible) {
return;
}
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);
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}
/>
)}
{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>
);
}}
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>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
onClick={handleSubmit}
outlined
disabled={!selectedItem || selectedItem.length !== 1}
size="small"
label="Submit"
/>
</div>
</div>
</Dialog>
);
};

View File

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

View File

@@ -3,6 +3,7 @@ import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
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 { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
@@ -22,30 +23,21 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
outCommand,
} = useMapRootState();
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const system = getSystemById(systems, systemId);
const [name, setName] = useState('');
const [label, setLabel] = useState('');
const [temporaryName, setTemporaryName] = useState('');
const [description, setDescription] = useState('');
const inputRef = useRef<HTMLInputElement>();
useEffect(() => {
if (!system) {
return;
}
const labels = new LabelsManager(system.labels || '');
setName(system.name || '');
setLabel(labels.customLabel);
setDescription(system.description || '');
}, [system]);
const ref = useRef({ name, description, label, outCommand, systemId, system });
ref.current = { name, description, label, outCommand, systemId, system };
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system });
ref.current = { name, description, label, temporaryName, outCommand, systemId, system };
const handleSave = useCallback(() => {
const { name, description, label, outCommand, systemId, system } = ref.current;
const { name, description, label, temporaryName, outCommand, systemId, system } = ref.current;
const outLabel = new LabelsManager(system?.labels ?? '');
outLabel.updateCustomLabel(label);
@@ -58,6 +50,14 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
},
});
outCommand({
type: OutCommand.updateSystemTemporaryName,
data: {
system_id: systemId,
value: temporaryName,
},
});
outCommand({
type: OutCommand.updateSystemName,
data: {
@@ -93,6 +93,21 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
e.target.value = e.target.value.toUpperCase().replace(/[^A-Z0-9\-[\](){}]/g, '');
}, []);
// Attention: this effect should be call only on mount.
useEffect(() => {
const { system } = ref.current;
if (!system) {
return;
}
const labels = new LabelsManager(system.labels || '');
setName(system.name || '');
setLabel(labels.customLabel);
setTemporaryName(system.temporary_name || '');
setDescription(system.description || '');
}, []);
return (
<Dialog
header="System settings"
@@ -167,6 +182,35 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
</IconField>
</div>
{isTempSystemNameEnabled && (
<div className="flex flex-col gap-1">
<label htmlFor="username">Temporary Name</label>
<IconField>
{temporaryName !== '' && (
<WdImgButton
className="pi pi-trash text-red-400"
textSize={WdImageSize.large}
tooltip={{
content: 'Remove temporary name',
className: 'pi p-input-icon',
position: TooltipPosition.top,
}}
onClick={() => setTemporaryName('')}
/>
)}
<InputText
id="temporaryName"
aria-describedby="temporaryName"
autoComplete="off"
value={temporaryName}
maxLength={10}
onChange={e => setTemporaryName(e.target.value)}
/>
</IconField>
</div>
)}
<div className="flex flex-col gap-1">
<label htmlFor="username">Description</label>
<InputTextarea

View File

@@ -8,7 +8,6 @@
.RouteSystem {
width: 8px;
height: 8px;
background: #ffffff;
cursor: pointer;
transition: opacity 200ms;

View File

@@ -21,6 +21,11 @@ import { RoutesProvider, useRouteProvider } from './RoutesProvider.tsx';
import { ContextMenuSystemInfo, useContextMenuSystemInfoHandlers } from '@/hooks/Mapper/components/contexts';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { OutCommand } from '@/hooks/Mapper/types';
const sortByDist = (a: Route, b: Route) => {
const distA = a.has_connection ? a.systems?.length || 0 : Infinity;
@@ -163,6 +168,12 @@ export const RoutesWidgetContent = () => {
export const RoutesWidgetComp = () => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update } = useRouteProvider();
const {
data: { hubs = [] },
outCommand,
} = useMapRootState();
const preparedHubs = useMemo(() => hubs.map(x => parseInt(x)), [hubs]);
const isSecure = data.path_type === 'secure';
const handleSecureChange = useCallback(() => {
@@ -174,6 +185,23 @@ export const RoutesWidgetComp = () => {
const ref = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(ref, 155);
const [openAddSystem, setOpenAddSystem] = useState<boolean>(false);
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (preparedHubs.includes(item.value)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: item.value },
});
},
[hubs, outCommand],
);
return (
<Widget
@@ -181,6 +209,14 @@ export const RoutesWidgetComp = () => {
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">Routes</span>
<LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
onClick={onAddSystem}
tooltip={{
content: 'Click here to add new system to routes',
}}
/>
<WdTooltipWrapper content="Show shortest route">
<WdCheckbox
size="xs"
@@ -191,13 +227,26 @@ export const RoutesWidgetComp = () => {
classNameLabel={clsx('text-red-400')}
/>
</WdTooltipWrapper>
<WdImgButton className={PrimeIcons.SLIDERS_H} onClick={() => setRouteSettingsVisible(true)} />
<WdImgButton
className={PrimeIcons.SLIDERS_H}
onClick={() => setRouteSettingsVisible(true)}
tooltip={{
content: 'Click here to open Routes settings',
}}
/>
</LayoutEventBlocker>
</div>
}
>
<RoutesWidgetContent />
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
<AddSystemDialog
title="Add system to routes"
visible={openAddSystem}
setVisible={() => setOpenAddSystem(false)}
onSubmit={handleSubmitAddSystem}
/>
</Widget>
);
};

View File

@@ -2,7 +2,10 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import {
getGroupIdByRawGroup,
GROUPS_LIST,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { DataTable, DataTableRowClickEvent, DataTableRowMouseEvent, SortOrder } from 'primereact/datatable';
import { Column } from 'primereact/column';
@@ -122,13 +125,14 @@ export const SystemSignaturesContent = ({
}
const isCosmicSignature = x.kind === COSMIC_SIGNATURE;
const preparedGroup = getGroupIdByRawGroup(x.group);
if (isCosmicSignature) {
const showCosmicSignatures = settings.find(y => y.key === COSMIC_SIGNATURE)?.value;
if (showCosmicSignatures) {
return !x.group || groupSettings.find(y => y.key === x.group)?.value;
return !x.group || groupSettings.find(y => y.key === preparedGroup)?.value;
} else {
return !!x.group && groupSettings.find(y => y.key === x.group)?.value;
return !!x.group && groupSettings.find(y => y.key === preparedGroup)?.value;
}
}

View File

@@ -1,4 +1,12 @@
import { GroupType, SignatureGroup } from '@/hooks/Mapper/types';
import {
GroupType,
SignatureGroup,
SignatureGroupENG,
SignatureGroupRU,
SignatureKind,
SignatureKindENG,
SignatureKindRU,
} from '@/hooks/Mapper/types';
export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = 1000 * 60 * 10;
@@ -24,3 +32,43 @@ export const GROUPS: Record<SignatureGroup, GroupType> = {
[SignatureGroup.Wormhole]: { id: SignatureGroup.Wormhole, icon: '/icons/brackets/wormhole.png', ...wh },
[SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 },
};
export const MAPPING_GROUP_TO_ENG = {
// ENGLISH
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
// RUSSIAN
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
};
export const MAPPING_TYPE_TO_ENG = {
// ENGLISH
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindENG.Structure]: SignatureKind.Structure,
[SignatureKindENG.Ship]: SignatureKind.Ship,
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
[SignatureKindENG.Drone]: SignatureKind.Drone,
// RUSSIAN
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindRU.Structure]: SignatureKind.Structure,
[SignatureKindRU.Ship]: SignatureKind.Ship,
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
[SignatureKindRU.Drone]: SignatureKind.Drone,
};
export const getGroupIdByRawGroup = (val: string) => MAPPING_GROUP_TO_ENG[val as SignatureGroup];

View File

@@ -16,6 +16,8 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const { interfaceSettings } = useMapRootState();
const { isShowMenu } = interfaceSettings;
const themeClass = `${interfaceSettings.theme ?? 'default'}-theme`;
const [showOnTheMap, setShowOnTheMap] = useState(false);
const [showMapSettings, setShowMapSettings] = useState(false);
const mapInterface = <MapInterface />;
@@ -26,27 +28,29 @@ export const MapRootContent = ({}: MapRootContentProps) => {
useSkipContextMenu();
return (
<Layout map={<MapWrapper />}>
{!isShowMenu ? (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<div className="absolute top-0 left-0 w-[calc(100%-3.5rem)] h-full pointer-events-none">
<Topbar />
<div className={themeClass}>
<Layout map={<MapWrapper />}>
{!isShowMenu ? (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<div className="absolute top-0 left-0 w-[calc(100%-3.5rem)] h-full pointer-events-none">
<Topbar />
{mapInterface}
</div>
<div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto">
<RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
</div>
</div>
) : (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar>
<MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
</Topbar>
{mapInterface}
</div>
<div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto">
<RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
</div>
</div>
) : (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar>
<MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
</Topbar>
{mapInterface}
</div>
)}
<OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} />
<MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} />
</Layout>
)}
<OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} />
<MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} />
</Layout>
</div>
);
};

View File

@@ -3,8 +3,13 @@ import { Dialog } from 'primereact/dialog';
import { useCallback, useMemo, useState } from 'react';
import { TabPanel, TabView } from 'primereact/tabview';
import { PrettySwitchbox } from './components';
import { InterfaceStoredSettings, InterfaceStoredSettingsProps, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
InterfaceStoredSettingsProps,
useMapRootState,
InterfaceStoredSettings,
} from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { Dropdown } from 'primereact/dropdown';
export enum UserSettingsRemoteProps {
link_signature_on_splash = 'link_signature_on_splash',
@@ -37,96 +42,171 @@ export interface MapSettingsProps {
onHide: () => void;
}
type CheckboxesList = {
type SettingsListItem = {
prop: keyof UserSettings;
label: string;
}[];
type: 'checkbox' | 'dropdown';
options?: { label: string; value: string }[];
};
const COMMON_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowMinimap, label: 'Show Minimap' },
const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMinimap,
label: 'Show Minimap',
type: 'checkbox',
},
];
const SYSTEMS_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowKSpace, label: 'Highlight Low/High-security systems' },
{ prop: UserSettingsRemoteProps.select_on_spash, label: 'Auto-select splashed' },
const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowKSpace,
label: 'Highlight Low/High-security systems',
type: 'checkbox',
},
{
prop: UserSettingsRemoteProps.select_on_spash,
label: 'Auto-select splashed',
type: 'checkbox',
},
];
const SIGNATURES_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: UserSettingsRemoteProps.link_signature_on_splash, label: 'Link signature on splash' },
{ prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures, label: 'Show unsplashed signatures' },
const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.link_signature_on_splash,
label: 'Link signature on splash',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures,
label: 'Show unsplashed signatures',
type: 'checkbox',
},
];
const CONNECTIONS_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: UserSettingsRemoteProps.delete_connection_with_sigs, label: 'Delete connections to linked signatures' },
{ prop: InterfaceStoredSettingsProps.isThickConnections, label: 'Thicker connections' },
const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
label: 'Delete connections to linked signatures',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isThickConnections,
label: 'Thicker connections',
type: 'checkbox',
},
];
const UI_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowMenu, label: 'Enable compact map menu bar' },
{ prop: InterfaceStoredSettingsProps.isShowBackgroundPattern, label: 'Show background pattern' },
{ prop: InterfaceStoredSettingsProps.isSoftBackground, label: 'Enable soft background' },
const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMenu,
label: 'Enable compact map menu bar',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isShowBackgroundPattern,
label: 'Show background pattern',
type: 'checkbox',
},
{
prop: InterfaceStoredSettingsProps.isSoftBackground,
label: 'Enable soft background',
type: 'checkbox',
},
];
const THEME_OPTIONS = [
{ label: 'Default', value: 'default' },
{ label: 'Pathfinder', value: 'pathfinder' },
];
const THEME_SETTING: SettingsListItem = {
prop: 'theme',
label: 'Theme',
type: 'dropdown',
options: THEME_OPTIONS,
};
export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({ ...DEFAULT_REMOTE_SETTINGS });
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
...DEFAULT_REMOTE_SETTINGS,
});
const mergedSettings = useMemo(() => {
return {
...interfaceSettings,
...userRemoteSettings,
...interfaceSettings,
};
}, [userRemoteSettings, interfaceSettings]);
const handleShow = async () => {
const { user_settings } = await outCommand({
type: OutCommand.getUserSettings,
data: null,
});
setUserRemoteSettings({
...user_settings,
});
};
const handleChangeChecked = useCallback(
(prop: keyof UserSettings) => async (checked: boolean) => {
// @ts-ignore
if (UserSettingsRemoteList.includes(prop)) {
const handleSettingChange = useCallback(
async (prop: keyof UserSettings, value: boolean | string) => {
if (UserSettingsRemoteList.includes(prop as any)) {
const newRemoteSettings = {
...userRemoteSettings,
[prop]: checked,
[prop]: value,
};
await outCommand({
type: OutCommand.updateUserSettings,
data: newRemoteSettings,
});
setUserRemoteSettings(newRemoteSettings);
return;
} else {
setInterfaceSettings({
...interfaceSettings,
[prop]: value,
});
}
setInterfaceSettings({
...interfaceSettings,
[prop]: checked,
});
},
[interfaceSettings, outCommand, setInterfaceSettings, userRemoteSettings],
[userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings],
);
const renderCheckboxesList = (list: CheckboxesList) => {
return list.map(x => {
const renderSettingItem = (item: SettingsListItem) => {
const currentValue = mergedSettings[item.prop];
if (item.type === 'checkbox') {
return (
<PrettySwitchbox
key={x.prop}
label={x.label}
checked={mergedSettings[x.prop]}
setChecked={handleChangeChecked(x.prop)}
key={item.prop}
label={item.label}
checked={!!currentValue}
setChecked={(checked) => handleSettingChange(item.prop, checked)}
/>
);
});
}
if (item.type === 'dropdown' && item.options) {
return (
<div key={item.prop} className="flex items-center gap-2 mt-2">
<label className="text-sm">{item.label}:</label>
<Dropdown
className="text-sm"
value={currentValue}
options={item.options}
onChange={(e) => handleSettingChange(item.prop, e.value)}
placeholder="Select a theme"
/>
</div>
);
}
return null;
};
const renderSettingsList = (list: SettingsListItem[]) => {
return list.map(renderSettingItem);
};
return (
@@ -137,10 +217,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
style={{ width: '550px' }}
onShow={handleShow}
onHide={() => {
if (!show) {
return;
}
if (!show) return;
setActiveIndex(0);
onHide();
}}
@@ -150,25 +227,35 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
<div className={styles.verticalTabsContainer}>
<TabView
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
onTabChange={(e) => setActiveIndex(e.index)}
className={styles.verticalTabView}
>
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">{renderCheckboxesList(COMMON_CHECKBOXES_PROPS)}</div>
</TabPanel>
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{renderCheckboxesList(SYSTEMS_CHECKBOXES_PROPS)}
{renderSettingsList(COMMON_CHECKBOXES_PROPS)}
</div>
</TabPanel>
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}
</div>
</TabPanel>
<TabPanel header="Connections" headerClassName={styles.verticalTabHeader}>
{renderCheckboxesList(CONNECTIONS_CHECKBOXES_PROPS)}
{renderSettingsList(CONNECTIONS_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}>
{renderCheckboxesList(SIGNATURES_CHECKBOXES_PROPS)}
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
{renderCheckboxesList(UI_CHECKBOXES_PROPS)}
{renderSettingsList(UI_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
{renderSettingItem(THEME_SETTING)}
</TabPanel>
</TabView>
</div>

View File

@@ -2,7 +2,7 @@ import { Map } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useRef, useState } from 'react';
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal';
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import {
@@ -14,14 +14,18 @@ import classes from './MapWrapper.module.scss';
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { Node } from 'reactflow';
import { Node, XYPosition } from 'reactflow';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
// TODO: INFO - this component needs for abstract work with Map instance
export const MapWrapper = () => {
@@ -36,6 +40,7 @@ export const MapWrapper = () => {
isThickConnections,
isShowBackgroundPattern,
isSoftBackground,
theme,
},
} = useMapRootState();
const { deleteSystems } = useDeleteSystems();
@@ -47,6 +52,7 @@ export const MapWrapper = () => {
const [openSettings, setOpenSettings] = useState<string | null>(null);
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
@@ -123,6 +129,28 @@ export const MapWrapper = () => {
}
}, []);
const onAddSystem: OnMapAddSystemCallback = useCallback(({ coordinates }) => {
setOpenAddSystem(coordinates);
}, []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => {
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
emitMapEvent({
name: Commands.centerSystem,
data: item.value.toString(),
});
return;
}
await outCommand({
type: OutCommand.manualAddSystem,
data: { coordinates: openAddSystem, solar_system_id: item.value },
});
},
[openAddSystem, outCommand],
);
return (
<>
<Map
@@ -139,6 +167,8 @@ export const MapWrapper = () => {
isThickConnections={isThickConnections}
isShowBackgroundPattern={isShowBackgroundPattern}
isSoftBackground={isSoftBackground}
theme={theme}
onAddSystem={onAddSystem}
/>
{openSettings != null && (
@@ -153,6 +183,12 @@ export const MapWrapper = () => {
<SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} />
)}
<AddSystemDialog
visible={!!openAddSystem}
setVisible={() => setOpenAddSystem(null)}
onSubmit={handleSubmitAddSystem}
/>
<Connections selectedConnection={selectedConnection} onHide={() => setSelectedConnection(null)} />
<ContextMenuSystem

View File

@@ -31,12 +31,15 @@ const prepareEffects = (effects: Record<string, EffectRaw>, effectName: string,
return out;
};
let counter = 0;
export interface WHEffectViewProps {
effectName: string;
effectPower: number;
className?: string;
}
export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) => {
export const WHEffectView = ({ effectName, effectPower, className }: WHEffectViewProps) => {
const {
data: { effects },
} = useMapRootState();
@@ -49,7 +52,7 @@ export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) =>
[effectName, effectPower, effects],
);
const targetClass = `wh-effect-name${effectInfo.id}`;
const targetClass = useMemo(() => `wh-effect-name${effectInfo.id}-${counter++}`, []);
return (
<div className={classes.WHEffectViewRoot}>
@@ -84,7 +87,7 @@ export const WHEffectView = ({ effectName, effectPower }: WHEffectViewProps) =>
</div>
</FixedTooltip>
<div className={clsx('font-bold select-none cursor-help w-min-content', effectClass, targetClass)}>
<div className={clsx('font-bold select-none cursor-help w-min-content', effectClass, targetClass, className)}>
{effectName}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import { SystemSignature } from '@/hooks/Mapper/types';
import { SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import { MAPPING_TYPE_TO_ENG } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => {
const outArr: SystemSignature[] = [];
@@ -14,10 +14,12 @@ export const parseSignatures = (value: string, availableKeys: string[]): SystemS
continue;
}
const kind = MAPPING_TYPE_TO_ENG[sigArrInfo[1] as SignatureKind];
outArr.push({
eve_id: sigArrInfo[0],
kind: availableKeys.includes(sigArrInfo[1]) ? sigArrInfo[1] : COSMIC_SIGNATURE,
group: sigArrInfo[2],
kind: availableKeys.includes(kind) ? kind : SignatureKind.CosmicSignature,
group: sigArrInfo[2] as SignatureGroup,
name: sigArrInfo[3],
type: '',
});

View File

@@ -37,6 +37,7 @@ export enum InterfaceStoredSettingsProps {
isShowUnsplashedSignatures = 'isShowUnsplashedSignatures',
isShowBackgroundPattern = 'isShowBackgroundPattern',
isSoftBackground = 'isSoftBackground',
theme = 'theme',
}
export type InterfaceStoredSettings = {
@@ -47,6 +48,7 @@ export type InterfaceStoredSettings = {
isShowUnsplashedSignatures: boolean;
isShowBackgroundPattern: boolean;
isSoftBackground: boolean;
theme: string;
};
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
@@ -57,7 +59,8 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowUnsplashedSignatures: false,
isShowBackgroundPattern: true,
isSoftBackground: false,
};
theme: 'default',
}
export interface MapRootContextProps {
update: ContextStoreDataUpdate<MapRootData>;

View File

@@ -129,6 +129,7 @@ export enum OutCommand {
updateConnectionCustomInfo = 'update_connection_custom_info',
updateSignatures = 'update_signatures',
updateSystemName = 'update_system_name',
updateSystemTemporaryName = 'update_system_temporary_name',
updateSystemDescription = 'update_system_description',
updateSystemLabels = 'update_system_labels',
updateSystemLocked = 'update_system_locked',
@@ -153,6 +154,7 @@ export enum OutCommand {
getUserSettings = 'get_user_settings',
updateUserSettings = 'update_user_settings',
unlinkSignature = 'unlink_signature',
searchSystems = 'search_systems',
}
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;

View File

@@ -10,6 +10,15 @@ export enum SignatureGroup {
CombatSite = 'Combat Site',
}
export enum SignatureKind {
CosmicSignature = 'Cosmic Signature',
CosmicAnomaly = 'Cosmic Anomaly',
Structure = 'Structure',
Ship = 'Ship',
Deployable = 'Deployable',
Drone = 'Drone',
}
export type GroupType = {
id: string;
icon: string;
@@ -19,7 +28,7 @@ export type GroupType = {
export type SystemSignature = {
eve_id: string;
kind: string;
kind: SignatureKind;
name: string;
custom_info?: string;
description?: string;
@@ -30,3 +39,41 @@ export type SystemSignature = {
inserted_at?: string;
updated_at?: string;
};
export enum SignatureKindENG {
CosmicSignature = 'Cosmic Signature',
CosmicAnomaly = 'Cosmic Anomaly',
Structure = 'Structure',
Ship = 'Ship',
Deployable = 'Deployable',
Drone = 'Drone',
}
export enum SignatureKindRU {
CosmicSignature = 'Скрытый сигнал',
CosmicAnomaly = 'Космическая аномалия',
Structure = 'Сооружение',
Ship = 'Корабль',
Deployable = 'Полевые блоки',
Drone = 'Дрон',
}
export enum SignatureGroupENG {
CosmicSignature = 'Cosmic Signature',
Wormhole = 'Wormhole',
GasSite = 'Gas Site',
RelicSite = 'Relic Site',
DataSite = 'Data Site',
OreSite = 'Ore Site',
CombatSite = 'Combat Site',
}
export enum SignatureGroupRU {
CosmicSignature = 'Скрытый сигнал',
Wormhole = 'Червоточина',
GasSite = 'Газовый район',
RelicSite = 'Археологический район',
DataSite = 'Информационный район',
OreSite = 'Астероидный район',
CombatSite = 'Боевой район',
}

View File

@@ -116,7 +116,18 @@ export type SolarSystemRawType = {
tag: string | null;
status: number;
name: string | null;
temporary_name: string | null;
system_static_info: SolarSystemStaticInfoRaw;
system_signatures: SystemSignature[];
};
export type SearchSystemItem = {
class_title: string;
constellation_name: string;
label: string;
region_name: string;
system_static_info: SolarSystemStaticInfoRaw;
value: number;
};

View File

@@ -1,17 +1,14 @@
export default {
mounted() {
const hook = this;
const url = hook.el.dataset.url;
const button = hook.el;
const button = this.el;
button.addEventListener('click', function () {
// Get the URL from the data attribute
button.classList.remove('copied');
// Copy the URL to the clipboard
navigator.clipboard
.writeText(url)
.writeText(button.dataset.url)
.then(() => {
button.classList.add('copied');
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -48,6 +48,11 @@ port =
scheme = System.get_env("WEB_EXTERNAL_SCHEME", "http")
public_api_disabled =
config_dir
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|> String.to_existing_atom()
map_subscriptions_enabled =
config_dir
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
@@ -83,6 +88,7 @@ config :wanderer_app,
admins: admins,
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
public_api_disabled: public_api_disabled,
map_subscriptions_enabled: map_subscriptions_enabled,
wallet_tracking_enabled: wallet_tracking_enabled,
subscription_settings: %{

View File

@@ -21,6 +21,7 @@ defmodule WandererApp.Api.Map do
define(:update_options, action: :update_options)
define(:assign_owner, action: :assign_owner)
define(:mark_as_deleted, action: :mark_as_deleted)
define(:update_api_key, action: :update_api_key)
define(:by_id,
get_by: [:id],
@@ -122,6 +123,11 @@ defmodule WandererApp.Api.Map do
change(set_attribute(:deleted, true))
end
update :update_api_key do
accept [:public_api_key]
end
end
attributes do
@@ -141,6 +147,10 @@ defmodule WandererApp.Api.Map do
attribute :description, :string
attribute :personal_note, :string
attribute :public_api_key, :string do
allow_nil? true
end
attribute :hubs, {:array, :string} do
allow_nil?(true)

View File

@@ -14,31 +14,24 @@ defmodule WandererApp.Api.MapCharacterSettings do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:read_by_map,
action: :read_by_map
)
define(:by_map_filtered,
action: :by_map_filtered
)
define(:tracked_by_map_filtered,
action: :tracked_by_map_filtered
)
define(:tracked_by_map_all,
action: :tracked_by_map_all
)
define(:read_by_map, action: :read_by_map)
define(:by_map_filtered, action: :by_map_filtered)
define(:tracked_by_map_filtered, action: :tracked_by_map_filtered)
define(:tracked_by_map_all, action: :tracked_by_map_all)
define(:track, action: :track)
define(:untrack, action: :untrack)
define(:follow, action: :follow)
define(:unfollow, action: :unfollow)
end
actions do
default_accept [
:map_id,
:character_id,
:tracked
:tracked,
:followed
]
defaults [:create, :read, :update, :destroy]
@@ -76,6 +69,14 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :untrack do
change(set_attribute(:tracked, false))
end
update :follow do
change(set_attribute(:followed, true))
end
update :unfollow do
change(set_attribute(:followed, false))
end
end
attributes do
@@ -86,6 +87,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
allow_nil? true
end
attribute :followed, :boolean do
default false
allow_nil? true
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -46,6 +46,7 @@ defmodule WandererApp.Api.MapSystem do
define(:update_locked, action: :update_locked)
define(:update_status, action: :update_status)
define(:update_tag, action: :update_tag)
define(:update_temporary_name, action: :update_temporary_name)
define(:update_labels, action: :update_labels)
define(:update_position, action: :update_position)
define(:update_visible, action: :update_visible)
@@ -102,6 +103,10 @@ defmodule WandererApp.Api.MapSystem do
accept [:tag]
end
update :update_temporary_name do
accept [:temporary_name]
end
update :update_labels do
accept [:labels]
end
@@ -141,6 +146,10 @@ defmodule WandererApp.Api.MapSystem do
allow_nil? true
end
attribute :temporary_name, :string do
allow_nil? true
end
attribute :labels, :string do
allow_nil? true
end

View File

@@ -489,7 +489,7 @@ defmodule WandererApp.Character.Tracker do
defp maybe_update_location(
%{
character_id: character_id
character_id: character_id,
} =
state,
location

View File

@@ -9,6 +9,7 @@ defmodule WandererApp.Env do
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites, do: get_key(:invites, false)
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
def public_api_disabled?, do: get_key(:public_api_disabled, false)
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
def admins, do: get_key(:admins, [])
def admin_username, do: get_key(:admin_username)

View File

@@ -129,6 +129,12 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.update_system_tag/2, [update]})
def update_system_temporary_name(map_id, update) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.update_system_temporary_name/2, [update]})
def update_system_locked(map_id, update) when is_binary(map_id),
do:
map_id

View File

@@ -150,6 +150,8 @@ defmodule WandererApp.Map.Server.Impl do
defdelegate update_system_tag(state, update), to: SystemsImpl
defdelegate update_system_temporary_name(state, update), to: SystemsImpl
defdelegate update_system_locked(state, update), to: SystemsImpl
defdelegate update_system_labels(state, update), to: SystemsImpl
@@ -201,7 +203,7 @@ defmodule WandererApp.Map.Server.Impl do
| map: map |> WandererApp.Map.update_subscription_settings!(subscription_settings)
}
def handle_event(:update_characters, %{map_id: map_id} = state) do
def handle_event(:update_characters, state) do
Process.send_after(self(), :update_characters, @update_characters_timeout)
CharactersImpl.update_characters(state)
@@ -259,7 +261,7 @@ defmodule WandererApp.Map.Server.Impl do
state
end
def handle_event(:cleanup_systems, %{map_id: map_id} = state) do
def handle_event(:cleanup_systems, state) do
Process.send_after(self(), :cleanup_systems, @systems_cleanup_timeout)
state |> SystemsImpl.cleanup_systems()
@@ -320,6 +322,8 @@ defmodule WandererApp.Map.Server.Impl do
layout: options |> Map.get("layout", "left_to_right"),
store_custom_labels:
options |> Map.get("store_custom_labels", "false") |> String.to_existing_atom(),
show_temp_system_name:
options |> Map.get("show_temp_system_name", "false") |> String.to_existing_atom(),
restrict_offline_showing:
options |> Map.get("restrict_offline_showing", "false") |> String.to_existing_atom()
]
@@ -427,7 +431,8 @@ defmodule WandererApp.Map.Server.Impl do
"name" => name,
"position" => %{"x" => x, "y" => y},
"status" => status,
"tag" => tag
"tag" => tag,
"temporary_name" => temporary_name,
} = _system,
acc ->
acc
@@ -446,6 +451,7 @@ defmodule WandererApp.Map.Server.Impl do
})
|> update_system_status(%{solar_system_id: id |> String.to_integer(), status: status})
|> update_system_tag(%{solar_system_id: id |> String.to_integer(), tag: tag})
|> update_system_temporary_name(%{solar_system_id: id |> String.to_integer(), temporary_name: temporary_name})
|> update_system_locked(%{solar_system_id: id |> String.to_integer(), locked: locked})
|> update_system_labels(%{solar_system_id: id |> String.to_integer(), labels: labels})
end)

View File

@@ -15,13 +15,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: track_character
tracked: track_character,
followed: false
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
@@ -382,7 +381,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:character_location, %{solar_system_id: solar_system_id},
%{solar_system_id: old_solar_system_id}}
]
_ ->
[:skip]
end

View File

@@ -333,7 +333,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
solar_system_id: location.solar_system_id,
})
Impl.broadcast!(map_id, :add_connection, connection)
@@ -346,9 +346,20 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
{:error, :already_exists} ->
# Still broadcast location change in case of followed character
Impl.broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
})
:ok
{:error, error} ->
Logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end)
:ok
end
end
@@ -359,33 +370,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
def can_add_location(:none, _solar_system_id), do: false
def can_add_location(scope, solar_system_id) do
system_static_info =
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
system_static_info
_ ->
%{system_class: nil}
end
{:ok, system_static_info} = get_system_static_info(solar_system_id)
case scope do
:wormholes ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not is_prohibited_system_class?(system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
@wh_space |> Enum.member?(system_static_info.system_class)
:stargates ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not is_prohibited_system_class?(system_static_info.system_class) and
@known_space |> Enum.member?(system_static_info.system_class)
:all ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class))
not is_prohibited_system_class?(system_static_info.system_class)
_ ->
false
end
end
def is_prohibited_system_class?(system_class) do
@prohibited_system_classes |> Enum.member?(system_class)
end
def is_connection_exist(map_id, from_solar_system_id, to_solar_system_id),
do:
not is_nil(
@@ -414,24 +422,21 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
current_system_id: to_solar_system_id
})
system_static_info =
case WandererApp.CachedInfo.get_system_static_info(to_solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
system_static_info
_ ->
%{system_class: nil}
end
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
case scope do
:wormholes ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?() and to_solar_system_id != @jita and
from_solar_system_id != @jita
:stargates ->
not (@prohibited_system_classes |> Enum.member?(system_static_info.system_class)) and
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (known_jumps |> Enum.empty?())
end
end
@@ -447,6 +452,16 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
end
end
defp get_system_static_info(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->
{:ok, system_static_info}
_ ->
{:ok, %{system_class: nil}}
end
end
defp maybe_remove_connection(map_id, location, old_location)
when not is_nil(location) and not is_nil(old_location) and
location.solar_system_id != old_location.solar_system_id do

View File

@@ -122,6 +122,13 @@ defmodule WandererApp.Map.Server.SystemsImpl do
),
do: state |> update_system(:update_tag, [:tag], update)
def update_system_temporary_name(
state,
update
) do
state |> update_system(:update_temporary_name, [:temporary_name], update)
end
def update_system_locked(
state,
update
@@ -286,6 +293,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.update_visible!(%{visible: true})
|> WandererApp.MapSystemRepo.cleanup_tags()
|> WandererApp.MapSystemRepo.cleanup_temporary_name()
@ddrt.insert(
{existing_system.solar_system_id,
@@ -404,6 +412,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y})
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.cleanup_tags!()
|> WandererApp.MapSystemRepo.cleanup_temporary_name!()
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
end

View File

@@ -84,20 +84,22 @@ defmodule WandererApp.Maps do
id: id,
eve_id: eve_id,
corporation_ticker: corporation_ticker,
tracked: false
tracked: false,
followed: false
}
def map_character(
%{name: name, id: id, eve_id: eve_id, corporation_ticker: corporation_ticker} =
_character,
%{tracked: tracked} = _character_settings
%{tracked: tracked, followed: followed} = _character_settings
),
do: %{
name: name,
id: id,
eve_id: eve_id,
corporation_ticker: corporation_ticker,
tracked: tracked
tracked: tracked,
followed: followed
}
@decorate cacheable(

View File

@@ -24,11 +24,31 @@ defmodule WandererApp.MapCharacterSettingsRepo do
def get_tracked_by_map_all(map_id),
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
def get_by_map(map_id, character_id) do
case get_by_map_filtered(map_id, [character_id]) do
{:ok, [setting | _]} ->
{:ok, setting}
{:ok, []} ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
end
end
def track(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track()
def untrack(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack()
def track!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.track!()
def untrack!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.untrack!()
def follow(settings), do: settings |> WandererApp.Api.MapCharacterSettings.follow()
def unfollow(settings), do: settings |> WandererApp.Api.MapCharacterSettings.unfollow()
def follow!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.follow!()
def unfollow!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.unfollow!()
def destroy!(settings), do: settings |> WandererApp.Api.MapCharacterSettings.destroy!()
end

View File

@@ -4,6 +4,7 @@ defmodule WandererApp.MapRepo do
@default_map_options %{
"layout" => "left_to_right",
"store_custom_labels" => "false",
"show_temp_system_name" => "false",
"restrict_offline_showing" => "false"
}

View File

@@ -16,11 +16,13 @@ defmodule WandererApp.MapSystemRepo do
end
end
def get_all_by_map(map_id),
do: WandererApp.Api.MapSystem.read_all_by_map(%{map_id: map_id})
def get_all_by_map(map_id) do
WandererApp.Api.MapSystem.read_all_by_map(%{map_id: map_id})
end
def get_visible_by_map(map_id),
do: WandererApp.Api.MapSystem.read_visible_by_map(%{map_id: map_id})
def get_visible_by_map(map_id) do
WandererApp.Api.MapSystem.read_visible_by_map(%{map_id: map_id})
end
def remove_from_map(map_id, solar_system_id) do
WandererApp.Api.MapSystem.read_by_map_and_solar_system!(%{
@@ -59,6 +61,20 @@ defmodule WandererApp.MapSystemRepo do
})
end
def cleanup_temporary_name(system) do
system
|> WandererApp.Api.MapSystem.update_temporary_name(%{
temporary_name: nil
})
end
def cleanup_temporary_name!(system) do
system
|> WandererApp.Api.MapSystem.update_temporary_name!(%{
temporary_name: nil
})
end
def get_filtered_labels(labels, true) when is_binary(labels) do
labels
|> Jason.decode!()
@@ -99,6 +115,11 @@ defmodule WandererApp.MapSystemRepo do
system
|> WandererApp.Api.MapSystem.update_tag(update)
def update_temporary_name(system, update) do
system
|> WandererApp.Api.MapSystem.update_temporary_name(update)
end
def update_labels(system, update),
do:
system

View File

@@ -0,0 +1,47 @@
defmodule WandererAppWeb.CharacterActivity do
use WandererAppWeb, :live_component
use LiveViewEvents
@impl true
def mount(socket) do
{:ok, socket}
end
@impl true
def update(
assigns,
socket
) do
{:ok,
socket
|> handle_info_or_assign(assigns)}
end
def render(assigns) do
~H"""
<div id={@id}>
<.table class="!max-h-[80vh] !overflow-y-auto" id="activity-tbl" rows={@activity}>
<:col :let={row} label="Character">
<.character_item character={row.character} />
</:col>
<:col :let={row} label="Passages">
<%= row.count %>
</:col>
</.table>
</div>
"""
end
def character_item(assigns) do
~H"""
<div class="flex items-center gap-3">
<div class="avatar">
<div class="rounded-md w-12 h-12">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
</div>
</div>
<%= @character.name %>
</div>
"""
end
end

View File

@@ -393,6 +393,7 @@ defmodule WandererAppWeb.CoreComponents do
data-pc-name="checkbox"
data-pc-section="root"
>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
id={@id}
name={@name}

View File

@@ -0,0 +1,308 @@
defmodule WandererAppWeb.APIController do
use WandererAppWeb, :controller
import Ash.Query, only: [filter: 2]
alias WandererApp.Api
alias WandererApp.MapSystemRepo
alias WandererApp.MapCharacterSettingsRepo
alias WandererApp.Api.Character
alias WandererApp.CachedInfo
# -----------------------------------------------------------------
# Common
# -----------------------------------------------------------------
@doc """
GET /api/system-static-info
Requires 'id' (the solar_system_id)
Example:
GET /api/common/system_static?id=31002229
GET /api/common/system_static?id=31002229
"""
def show_system_static(conn, params) do
with {:ok, solar_system_str} <- require_param(params, "id"),
{:ok, solar_system_id} <- parse_int(solar_system_str) do
case CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system} ->
data = static_system_to_json(system)
json(conn, %{data: data})
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "System not found"})
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
# -----------------------------------------------------------------
# Map
# -----------------------------------------------------------------
@doc """
GET /api/map/systems
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
If `?all=true` is provided, **all** systems are returned.
Otherwise, only "visible" systems are returned.
Examples:
GET /api/map/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/systems?slug=my-unique-wormhole-map
GET /api/map/systems?map_id=<UUID>&all=true
"""
def list_systems(conn, params) do
with {:ok, map_id} <- fetch_map_id(params) do
# Decide which function to call based on the "all" param
repo_fun =
if params["all"] == "true" do
&MapSystemRepo.get_all_by_map/1
else
&MapSystemRepo.get_visible_by_map/1
end
case repo_fun.(map_id) do
{:ok, systems} ->
data = Enum.map(systems, &map_system_to_json/1)
json(conn, %{data: data})
{:error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "Could not fetch systems for map_id=#{map_id}: #{inspect(reason)}"})
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
@doc """
GET /api/map/system
Requires 'id' (the solar_system_id)
plus either ?map_id=<UUID> or ?slug=<map-slug>.
Example:
GET /api/map/system?id=31002229&map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/system?id=31002229&slug=my-unique-wormhole-map
"""
def show_system(conn, params) do
with {:ok, solar_system_str} <- require_param(params, "id"),
{:ok, solar_system_id} <- parse_int(solar_system_str),
{:ok, map_id} <- fetch_map_id(params) do
case MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
{:ok, system} ->
data = map_system_to_json(system)
json(conn, %{data: data})
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "System not found in map=#{map_id}"})
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
@doc """
GET /api/map/tracked_characters_with_info
Example usage:
GET /api/map/tracked_characters_with_info?map_id=<uuid>
GET /api/map/tracked_characters_with_info?slug=<map-slug>
Returns a list of tracked records, plus their fully-loaded `character` data.
"""
def tracked_characters_with_info(conn, params) do
with {:ok, map_id} <- fetch_map_id(params),
{:ok, settings_list} <- get_tracked_by_map_ids(map_id),
{:ok, char_list} <-
read_characters_by_ids_wrapper(Enum.map(settings_list, & &1.character_id)) do
chars_by_id = Map.new(char_list, &{&1.id, &1})
data =
Enum.map(settings_list, fn setting ->
found_char = Map.get(chars_by_id, setting.character_id)
%{
id: setting.id,
map_id: setting.map_id,
character_id: setting.character_id,
tracked: setting.tracked,
inserted_at: setting.inserted_at,
updated_at: setting.updated_at,
character:
if found_char do
character_to_json(found_char)
else
%{}
end
}
end)
json(conn, %{data: data})
else
{:error, :get_tracked_error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "No tracked records found for map_id: #{inspect(reason)}"})
{:error, :read_characters_by_ids_error, reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Could not load Character records: #{inspect(reason)}"})
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{error: message})
end
end
defp get_tracked_by_map_ids(map_id) do
case MapCharacterSettingsRepo.get_tracked_by_map_all(map_id) do
{:ok, settings_list} ->
{:ok, settings_list}
{:error, reason} ->
{:error, :get_tracked_error, reason}
end
end
defp read_characters_by_ids_wrapper(ids) do
case read_characters_by_ids(ids) do
{:ok, char_list} ->
{:ok, char_list}
{:error, reason} ->
{:error, :read_characters_by_ids_error, reason}
end
end
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
{:ok, mid}
end
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
case WandererApp.Api.Map.get_map_by_slug(slug) do
{:ok, map} ->
{:ok, map.id}
{:error, _reason} ->
{:error, "No map found for slug=#{slug}"}
end
end
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
defp require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
defp parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
defp read_characters_by_ids(ids) when is_list(ids) do
if ids == [] do
{:ok, []}
else
query =
Character
|> filter(id in ^ids)
Api.read(query)
end
end
defp map_system_to_json(system) do
Map.take(system, [
:id,
:map_id,
:solar_system_id,
:name,
:custom_name,
:temporary_name,
:description,
:tag,
:labels,
:locked,
:visible,
:status,
:position_x,
:position_y,
:inserted_at,
:updated_at
])
end
defp character_to_json(ch) do
Map.take(ch, [
:id,
:eve_id,
:name,
:corporation_id,
:corporation_name,
:corporation_ticker,
:alliance_id,
:alliance_name,
:alliance_ticker,
:inserted_at,
:updated_at
])
end
defp static_system_to_json(system) do
system
|> Map.take([
:solar_system_id,
:region_id,
:constellation_id,
:solar_system_name,
:solar_system_name_lc,
:constellation_name,
:region_name,
:system_class,
:security,
:type_description,
:class_title,
:is_shattered,
:effect_name,
:effect_power,
:statics,
:wandering,
:triglavian_invasion_status,
:sun_type_id
])
end
end

View File

@@ -0,0 +1,15 @@
defmodule WandererAppWeb.Plugs.CheckApiDisabled do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if WandererApp.Env.public_api_disabled?() do
conn
|> send_resp(403, "Public API is disabled")
|> halt()
else
conn
end
end
end

View File

@@ -0,0 +1,62 @@
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
@moduledoc """
A plug that checks the "Authorization: Bearer <token>" header
against the maps stored public_api_key. Halts with 401 if invalid.
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
header = get_req_header(conn, "authorization") |> List.first()
case header do
"Bearer " <> incoming_token ->
case fetch_map_id(conn.query_params) do
{:ok, map_id} ->
case WandererApp.Api.Map.by_id(map_id) do
{:ok, map} ->
if map.public_api_key == incoming_token do
conn
else
conn
|> send_resp(401, "Unauthorized (invalid token for map)")
|> halt()
end
{:error, _reason} ->
conn
|> send_resp(404, "Map not found")
|> halt()
end
{:error, msg} ->
conn
|> send_resp(400, msg)
|> halt()
end
_ ->
conn
|> send_resp(401, "Missing or invalid 'Bearer' token")
|> halt()
end
end
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
{:ok, mid}
end
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
case WandererApp.Api.Map.get_map_by_slug(slug) do
{:ok, map} ->
{:ok, map.id}
{:error, _reason} ->
{:error, "No map found for slug=#{slug}"}
end
end
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
end

View File

@@ -86,7 +86,8 @@ defmodule WandererAppWeb.CharactersTrackingLive do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: selected_map.id,
tracked: true
tracked: true,
followed: false
})
{:noreply, socket}

View File

@@ -18,15 +18,12 @@ defmodule WandererAppWeb.MapActivityEventHandler do
do: MapCoreEventHandler.handle_server_event(event, socket)
def handle_ui_event("show_activity", _, %{assigns: %{map_id: map_id}} = socket) do
Task.async(fn ->
{:ok, character_activity} = map_id |> get_character_activity()
{:character_activity, character_activity}
end)
{:noreply,
socket
|> assign(:show_activity?, true)}
|> assign(:show_activity?, true)
|> assign_async(:character_activity, fn ->
map_id |> get_character_activity()
end)}
end
def handle_ui_event("hide_activity", _, socket),
@@ -44,6 +41,6 @@ defmodule WandererAppWeb.MapActivityEventHandler do
%{p | character: p.character |> MapEventHandler.map_ui_character_stat()}
end)
{:ok, %{jumps: jumps}}
{:ok, %{character_activity: jumps}}
end
end

View File

@@ -111,7 +111,8 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: true
tracked: true,
followed: false
})
character = map_character_settings |> Ash.load!(:character) |> Map.get(:character)
@@ -190,6 +191,90 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
)}
end
def handle_ui_event(
"toggle_follow",
%{"character-id" => clicked_char_id},
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} = socket
) do
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
# Find and filter user's characters
{:ok, user_characters} = get_tracked_map_characters(map_id, current_user)
user_char_ids = Enum.map(user_characters, & &1.id)
my_settings =
all_settings
|> Enum.filter(fn s ->
s.character_id in user_char_ids
end)
existing = Enum.find(my_settings, &(&1.character_id == clicked_char_id))
{:ok, target_setting} =
if not is_nil(existing) do
{:ok, existing}
else
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: clicked_char_id,
map_id: map_id,
tracked: true,
followed: true
})
end
# If the target_setting is already followed => unfollow it
if target_setting.followed do
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.unfollow(target_setting)
else
# Only unfollow other rows from the current user
for s <- my_settings, s.id != target_setting.id, s.followed == true do
WandererApp.MapCharacterSettingsRepo.unfollow!(s)
end
# Ensure the new followed char is tracked
if not target_setting.tracked do
WandererApp.MapCharacterSettingsRepo.track!(target_setting)
char = target_setting |> Ash.load!(:character) |> Map.get(:character)
:ok = track_characters([char], map_id, true)
:ok = add_characters([char], map_id, true)
end
{:ok, updated} = WandererApp.MapCharacterSettingsRepo.follow(target_setting)
end
# re-fetch or re-map to confirm final results in UI
%{result: characters} = socket.assigns.characters
{:ok, tracked_characters} = get_tracked_map_characters(map_id, current_user)
user_eve_ids = Enum.map(tracked_characters, & &1.eve_id)
{:ok, final_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
updated_chars =
characters
|> Enum.map(fn c ->
s = Enum.find(final_settings, &(&1.character_id == c.id))
WandererApp.Maps.map_character(c, s)
end)
socket =
socket
|> assign(user_characters: user_eve_ids)
|> assign(has_tracked_characters?: has_tracked_characters?(user_eve_ids))
|> assign_async(:characters, fn ->
{:ok, %{characters: updated_chars}}
end)
|> MapEventHandler.push_map_event("init", %{user_characters: user_eve_ids, reset: false})
{:noreply, socket}
end
def handle_ui_event("hide_tracking", _, socket),
do: {:noreply, socket |> assign(show_tracking?: false)}

View File

@@ -3,7 +3,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
use Phoenix.Component
require Logger
alias WandererAppWeb.{MapEventHandler, MapCharactersEventHandler}
alias WandererAppWeb.{MapEventHandler, MapCharactersEventHandler, MapSystemsEventHandler}
def handle_server_event(:update_permissions, socket) do
DebounceAndThrottle.Debounce.apply(
@@ -147,7 +147,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
options =
WandererApp.Api.MapSolarSystem.find_by_name!(%{name: text})
|> Enum.take(100)
|> Enum.map(&map_system/1)
|> Enum.map(&MapSystemsEventHandler.map_system/1)
send_update(LiveSelect.Component, options: options, id: id)
@@ -162,6 +162,14 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
)
def handle_ui_event("toggle_follow_" <> character_id, _, socket),
do:
MapCharactersEventHandler.handle_ui_event(
"toggle_follow",
%{"character-id" => character_id},
socket
)
def handle_ui_event(
"get_user_settings",
_,
@@ -226,7 +234,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|> MapCharactersEventHandler.add_character()}
def handle_ui_event(event, body, socket) do
Logger.warning(fn -> "unhandled map ui event: #{event} #{inspect(body)}" end)
Logger.warning(fn -> "unhandled map ui event: #{inspect(event)} #{inspect(body)}" end)
{:noreply, socket}
end
@@ -564,21 +572,4 @@ defmodule WandererAppWeb.MapCoreEventHandler do
user_character_eve_ids |> Enum.member?(character.eve_id)
end)
end
defp map_system(
%{
solar_system_name: solar_system_name,
constellation_name: constellation_name,
region_name: region_name,
solar_system_id: solar_system_id,
class_title: class_title
} = _system
),
do: %{
label: solar_system_name,
value: solar_system_id,
constellation_name: constellation_name,
region_name: region_name,
class_title: class_title
}
end

View File

@@ -21,32 +21,57 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|> MapEventHandler.push_map_event("remove_systems", solar_system_ids)
def handle_server_event(
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_user_settings: map_user_settings}} = socket
) do
is_user_character =
current_user.characters |> Enum.map(& &1.id) |> Enum.member?(character_id)
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_id: map_id, map_user_settings: map_user_settings}} = socket
) do
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_user_character =
current_user.characters
|> Enum.map(& &1.id)
|> Enum.member?(character_id)
(is_user_character && is_select_on_spash)
|> case do
true ->
is_select_on_spash =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
is_followed =
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character_id) do
{:ok, setting} -> setting.followed == true
_ -> false
end
must_select? = is_user_character && (is_select_on_spash || is_followed)
if not must_select? do
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
else
# Check if we already selected this exact system for this char:
last_selected =
WandererApp.Cache.lookup!(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
nil
)
false ->
socket
end
if last_selected == solar_system_id do
# same system => skip
socket
else
# new system => update cache + push event
WandererApp.Cache.put(
"char:#{character_id}:map:#{map_id}:last_selected_system_id",
solar_system_id
)
socket
|> MapEventHandler.push_map_event("select_system", solar_system_id)
end
end
end
def handle_server_event(%{event: :kills_updated, payload: kills}, socket) do
@@ -65,55 +90,32 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
do: MapCoreEventHandler.handle_server_event(event, socket)
def handle_ui_event(
"add_system",
%{"system_id" => [solar_system_id]} = _event,
"manual_add_system",
%{"solar_system_id" => solar_system_id, "coordinates" => coordinates} = _event,
%{
assigns:
%{
map_id: map_id,
map_slug: map_slug,
current_user: current_user,
tracked_character_ids: tracked_character_ids,
user_permissions: %{add_system: true}
} = assigns
} = socket
)
when is_binary(solar_system_id) and solar_system_id != "" do
coordinates = Map.get(assigns, :coordinates)
assigns: %{
current_user: current_user,
has_tracked_characters?: true,
map_id: map_id,
tracked_character_ids: tracked_character_ids,
user_permissions: %{add_system: true}
}
} =
socket
) do
WandererApp.Map.Server.add_system(
map_id,
%{
solar_system_id: solar_system_id |> String.to_integer(),
solar_system_id: solar_system_id,
coordinates: coordinates
},
current_user.id,
tracked_character_ids |> List.first()
)
{:noreply,
socket
|> push_patch(to: ~p"/#{map_slug}")}
{:noreply, socket}
end
def handle_ui_event(
"manual_add_system",
%{"coordinates" => coordinates} = _event,
%{
assigns: %{
has_tracked_characters?: true,
map_slug: map_slug,
user_permissions: %{add_system: true}
}
} =
socket
),
do:
{:noreply,
socket
|> assign(coordinates: coordinates)
|> push_patch(to: ~p"/#{map_slug}/add-system")}
def handle_ui_event(
"add_hub",
%{"system_id" => solar_system_id} = _event,
@@ -229,6 +231,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
"labels" -> :update_system_labels
"locked" -> :update_system_locked
"tag" -> :update_system_tag
"temporary_name" -> :update_system_temporary_name
"status" -> :update_system_status
_ -> nil
end
@@ -240,6 +243,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
"labels" -> :labels
"locked" -> :locked
"tag" -> :tag
"temporary_name" -> :temporary_name
"status" -> :status
_ -> :none
end
@@ -280,6 +284,25 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
{:reply, %{system_static_infos: system_static_infos}, socket}
end
def handle_ui_event(
"search_systems",
%{"text" => text} = _event,
socket
) do
systems =
WandererApp.Api.MapSolarSystem.find_by_name!(%{name: text})
|> Enum.take(100)
|> Enum.map(&map_system/1)
|> Enum.filter(fn system ->
not is_nil(system) && not is_nil(system.system_static_info) &&
not WandererApp.Map.Server.ConnectionsImpl.is_prohibited_system_class?(
system.system_static_info.system_class
)
end)
{:reply, %{systems: systems}, socket}
end
def handle_ui_event(
"delete_systems",
solar_system_ids,
@@ -307,6 +330,27 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
def map_system(
%{
solar_system_name: solar_system_name,
constellation_name: constellation_name,
region_name: region_name,
solar_system_id: solar_system_id,
class_title: class_title
} = _system
) do
system_static_info = MapEventHandler.get_system_static_info(solar_system_id)
%{
label: solar_system_name,
value: solar_system_id,
constellation_name: constellation_name,
region_name: region_name,
class_title: class_title,
system_static_info: system_static_info
}
end
defp can_update_system?(:locked, %{lock_system: false} = _user_permissions), do: false
defp can_update_system?(_key, _user_permissions), do: true

View File

@@ -24,6 +24,7 @@ defmodule WandererAppWeb.MapEventHandler do
@map_characters_ui_events [
"add_character",
"toggle_track",
"toggle_follow",
"hide_tracking"
]
@@ -38,10 +39,10 @@ defmodule WandererAppWeb.MapEventHandler do
@map_system_ui_events [
"add_hub",
"delete_hub",
"add_system",
"delete_systems",
"manual_add_system",
"get_system_static_infos",
"manual_add_system",
"search_systems",
"update_system_position",
"update_system_positions",
"update_system_name",
@@ -49,6 +50,7 @@ defmodule WandererAppWeb.MapEventHandler do
"update_system_labels",
"update_system_locked",
"update_system_tag",
"update_system_temporary_name",
"update_system_status"
]
@@ -243,6 +245,7 @@ defmodule WandererAppWeb.MapEventHandler do
locked: locked,
tag: tag,
labels: labels,
temporary_name: temporary_name,
status: status,
visible: visible
} = _system,
@@ -268,6 +271,7 @@ defmodule WandererAppWeb.MapEventHandler do
locked: locked,
status: status,
tag: tag,
temporary_name: temporary_name,
visible: visible
}
end

View File

@@ -77,13 +77,14 @@ defmodule WandererAppWeb.MapLive do
def handle_info(:not_all_characters_tracked, %{assigns: %{map_slug: map_slug}} = socket),
do:
WandererAppWeb.MapEventHandler.handle_ui_event(
%{event: "add_character"},
"add_character",
nil,
socket
|> put_flash(
:error,
"You should enable tracking for all characters that have access to this map first!"
)
|> push_navigate(to: ~p"/tracking/#{map_slug}")
)
@impl true
@@ -101,24 +102,4 @@ defmodule WandererAppWeb.MapLive do
socket
|> assign(:active_page, :map)
end
defp apply_action(socket, :add_system, _params) do
socket
|> assign(:active_page, :map)
|> assign(:page_title, "Add System")
|> assign(:add_system_form, to_form(%{"system_id" => nil}))
end
def character_item(assigns) do
~H"""
<div class="flex items-center gap-3">
<div class="avatar">
<div class="rounded-md w-12 h-12">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
</div>
</div>
<%= @character.name %>
</div>
"""
end
end

View File

@@ -31,41 +31,6 @@
</.link>
</div>
<.modal
:if={@live_action in [:add_system] && not is_nil(assigns |> Map.get(:map_slug)) && @map_loaded?}
id="add-system-modal"
class="!w-[400px]"
title="Add System"
show
on_cancel={JS.patch(~p"/#{@map_slug}")}
>
<.form :let={f} for={@add_system_form} phx-submit="add_system">
<.live_select
label="Search system"
field={f[:system_id]}
update_min_len={2}
available_option_class="w-full text-sm"
debounce={200}
mode={:tags}
>
<:option :let={option}>
<div class="gap-1 w-full flex flex-align-center p-autocomplete-item text-sm">
<div class="eve-wh-type-color-c1 text-gray-400 w-8"><%= option.class_title %></div>
<div class="text-white w-16"><%= option.label %></div>
<div class="text-gray-600 w-20"><%= option.constellation_name %></div>
<div class="text-gray-600"><%= option.region_name %></div>
</div>
</:option>
</.live_select>
<div class="mt-2 bg-neutral text-neutral-content rounded-md p-1 text-xs w-full">
* Start search system. You should type at least 2 symbols.
</div>
<div class="modal-action mt-0">
<.button class="mt-2" type="submit">Add</.button>
</div>
</.form>
</.modal>
<.modal
:if={assigns |> Map.get(:show_activity?, false)}
id="map-activity-modal"
@@ -74,31 +39,35 @@
show
on_cancel={JS.push("hide_activity")}
>
<.table
:if={not (assigns |> Map.get(:character_activity) |> is_nil())}
class="!max-h-[80vh] !overflow-y-auto"
id="activity-tbl"
rows={@character_activity.jumps}
>
<:col :let={activity} label="Character">
<.character_item character={activity.character} />
</:col>
<:col :let={activity} label="Passages">
<%= activity.count %>
</:col>
</.table>
<.async_result :let={character_activity} assign={@character_activity}>
<:loading>Loading...</:loading>
<:failed :let={reason}><%= reason %></:failed>
<span :if={character_activity}>
<.live_component
module={WandererAppWeb.CharacterActivity}
id="character-activity"
activity={character_activity}
notify_to={self()}
/>
</span>
</.async_result>
</.modal>
<.modal
:if={assigns |> Map.get(:show_tracking?, false)}
id="map-tracking-modal"
title="Track Characters"
title="Track and Follow Characters"
show
on_cancel={JS.push("hide_tracking")}
>
<.async_result :let={characters} assign={@characters}>
<:loading><span class="loading loading-dots loading-xs" /></:loading>
<:failed :let={reason}><%= reason %></:failed>
<:loading>
<span class="loading loading-dots loading-xs" />
</:loading>
<:failed :let={reason}>
<%= reason %>
</:failed>
<.table
:if={characters}
@@ -107,7 +76,7 @@
rows={characters}
>
<:col :let={character} label="Track">
<label class="flex items-center gap-3">
<label class="flex items-center gap-2 justify-center">
<input
type="checkbox"
class="checkbox"
@@ -116,17 +85,34 @@
id={"character-track-#{character.id}"}
checked={character.tracked}
/>
<div class="flex items-center gap-3">
<.avatar url={member_icon_url(character.eve_id)} label={character.name} />
<div>
<div class="font-bold">
<%= character.name %><span class="ml-1 text-gray-400">[<%= character.corporation_ticker %>]</span>
</div>
<div class="text-sm opacity-50"></div>
</div>
</div>
</label>
</:col>
<:col :let={character} label="Follow">
<label class="flex items-center gap-2 justify-center">
<input
type="radio"
name="followed_character"
class="radio"
phx-click="toggle_follow"
phx-value-character-id={character.id}
checked={Map.get(character, :followed, false)}
/>
</label>
</:col>
<:col :let={character} label="Character">
<div class="flex items-center gap-3">
<.avatar url={member_icon_url(character.eve_id)} label={character.name} />
<div>
<div class="font-bold">
<%= character.name %>
<span class="ml-1 text-gray-400">
[<%= character.corporation_ticker %>]
</span>
</div>
<div class="text-sm opacity-50"></div>
</div>
</div>
</:col>
</.table>
</.async_result>
</.modal>

View File

@@ -127,6 +127,7 @@ defmodule WandererAppWeb.MapsLive do
|> assign(:page_title, "Maps - Settings")
|> assign(:map_slug, map_slug)
|> assign(:map_id, map.id)
|> assign(:public_api_key, map.public_api_key)
|> assign(:map, map)
|> assign(
export_settings: export_settings |> _get_export_map_data(),
@@ -179,6 +180,19 @@ defmodule WandererAppWeb.MapsLive do
{:noreply, socket}
end
def handle_event("generate-map-api-key", _params, socket) do
new_api_key = UUID.uuid4()
map = WandererApp.Api.Map.by_id!(socket.assigns.map_id)
{:ok, _updated_map} =
WandererApp.Api.Map.update_api_key(map, %{public_api_key: new_api_key})
{:noreply, assign(socket, public_api_key: new_api_key)}
end
@impl true
def handle_event(
"live_select_change",
@@ -203,7 +217,10 @@ defmodule WandererAppWeb.MapsLive do
socket.assigns.form,
form
|> Map.put("acls", form["acls"] || [])
|> Map.put("only_tracked_characters", form["only_tracked_characters"] || false)
|> Map.put(
"only_tracked_characters",
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
)
)
{:noreply, socket |> assign(form: form)}
@@ -593,7 +610,13 @@ defmodule WandererAppWeb.MapsLive do
scope -> scope
end
form = form |> Map.put("scope", scope)
form =
form
|> Map.put("scope", scope)
|> Map.put(
"only_tracked_characters",
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
)
map
|> WandererApp.Api.Map.update(form)
@@ -659,7 +682,12 @@ defmodule WandererAppWeb.MapsLive do
) do
options =
options_form
|> Map.take(["layout", "store_custom_labels", "restrict_offline_showing"])
|> Map.take([
"layout",
"store_custom_labels",
"show_temp_system_name",
"restrict_offline_showing"
])
{:ok, updated_map} = WandererApp.MapRepo.update_options(map, options)

View File

@@ -313,6 +313,32 @@
style="width: 146px; left: 0px;"
>
</li>
<li
:if={not WandererApp.Env.public_api_disabled?()}
class={[
"p-unselectable-text",
classes("p-tabview-selected p-highlight": @active_settings_tab == "public_api")
]}
role="presentation"
data-pc-name=""
data-pc-section="header"
>
<a
role="tab"
class="p-tabview-nav-link flex p-[10px]"
tabindex="-1"
aria-controls="pr_id_335_content"
aria-selected="false"
aria-disabled="false"
data-pc-section="headeraction"
phx-click="change_settings_tab"
phx-value-tab="public_api"
>
<span class="p-tabview-title" data-pc-section="headertitle">
<.icon name="hero-globe-alt-solid" class="w-4 h-4" />&nbsp;Public Api
</span>
</a>
</li>
</ul>
</div>
</div>
@@ -345,6 +371,11 @@
field={f[:store_custom_labels]}
label="Store system custom labels"
/>
<.input
type="checkbox"
field={f[:show_temp_system_name]}
label="Allow Temporary System Names"
/>
<.input
type="checkbox"
field={f[:restrict_offline_showing]}
@@ -377,6 +408,52 @@
</.button>
</div>
<div :if={@active_settings_tab == "public_api"and not WandererApp.Env.public_api_disabled?()} class="p-6">
<h2 class="text-lg font-semibold mb-4">Public API</h2>
<div class="flex flex-col gap-3 items-start w-full">
<div>
<input
:if={not is_nil(@public_api_key)}
class="input input-bordered text-sm truncate bg-neutral-800 text-white w-[350px]"
readonly
type="text"
value={@public_api_key}
/>
<input
:if={is_nil(@public_api_key)}
class="input input-bordered text-sm truncate bg-neutral-800 text-gray-400 w-[350px]"
readonly
type="text"
placeholder="No Public API Key yet"
/>
</div>
<div class="flex items-center gap-2">
<.button
class="btn btn-primary rounded-md"
phx-click="generate-map-api-key"
>
Generate
</.button>
<.button
phx-hook="CopyToClipboard"
id="copy-map-api-key"
data-url={@public_api_key}
disabled={is_nil(@public_api_key)}
class={
if is_nil(@public_api_key) do
"copy-link btn rounded-md transition-colors duration-300
bg-gray-500 hover:bg-gray-500 text-gray-300 cursor-not-allowed"
else
"copy-link btn rounded-md transition-colors duration-300
bg-blue-600 hover:bg-blue-700 text-white cursor-pointer"
end
}
>
Copy
</.button>
</div>
</div>
</div>
<div :if={@active_settings_tab == "balance"}>
<div class="stats w-full bg-primary text-primary-content">
<div class="stat">

View File

@@ -21,9 +21,10 @@ defmodule WandererAppWeb.Router do
@frame_src if(@code_reloading, do: ~w('self'), else: ~w())
@style_src ~w('self' 'unsafe-inline' https://fonts.googleapis.com)
@img_src ~w('self' data: https://images.evetech.net https://web.ccpgamescdn.com https://images.ctfassets.net https://w.appzi.io)
@font_src ~w('self' data: https://web.ccpgamescdn.com https://w.appzi.io)
@font_src ~w('self' https://fonts.gstatic.com data: https://web.ccpgamescdn.com https://w.appzi.io )
@script_src ~w('self' )
pipeline :admin_bauth do
plug :admin_basic_auth
end
@@ -106,8 +107,38 @@ defmodule WandererAppWeb.Router do
pipeline :api do
plug(:accepts, ["json"])
plug WandererAppWeb.Plugs.CheckApiDisabled
end
pipeline :api_map do
plug WandererAppWeb.Plugs.CheckMapApiKey
end
scope "/api/map", WandererAppWeb do
pipe_through [:api_map]
pipe_through [:api]
# GET /api/map/systems?map_id=... or ?slug=...
get "/systems", APIController, :list_systems
# GET /api/map/system-static-info?id=... plus either map_id=... or slug=...
get "/system-static-info", APIController, :show_system_static
# GET /api/map/system?id=... plus either map_id=... or slug=...
get "/system", APIController, :show_system
# GET /api/map/characters?map_id=... or slug=...
get "/characters", APIController, :tracked_characters_with_info
end
scope "/api/common", WandererAppWeb do
pipe_through [:api]
# GET /api/common/system-static-info?id=...
get "/system-static-info", APIController, :show_system_static
end
scope "/", WandererAppWeb do
pipe_through [:browser, :blog, :redirect_if_user_is_authenticated]
@@ -197,7 +228,6 @@ defmodule WandererAppWeb.Router do
live("/profile/deposit", ProfileLive, :deposit)
live("/profile/subscribe", ProfileLive, :subscribe)
live("/:slug/audit", MapAuditLive, :index)
live("/:slug/add-system", MapLive, :add_system)
live("/:slug", MapLive, :index)
end
end

View File

@@ -2,7 +2,8 @@ defmodule WandererApp.MixProject do
use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.31.0"
@version "1.37.6"
def project do
[

View File

@@ -0,0 +1,209 @@
%{
title: "User Guide: Public API Endpoints for Map Data",
author: "Wanderer Team",
cover_image_uri: "/images/news/01-05-map-public-api/generate-key.png",
tags: ~w(map public-api guide interface),
description: "Learn how to use the Wanderer public API endpoints to retrieve system and character data from your map. This guide covers available endpoints, request examples, and sample responses."
}
---
## Introduction
As part of the Wanderer platform, a public API has been introduced to help users programmatically retrieve map data, such as system information and character tracking details. This guide explains how to use these endpoints, how to authenticate with the API, and what data to expect in the responses.
**Important:** To use these endpoints, you need a valid API key for the map in question. You can generate or copy this key from within the **Map Settings** modal in the app:
![Generate Map API Key](/images/news/01-05-map-public-api/generate-key.png "Generate Map API Key")
---
## Authentication
Each request to the Wanderer APIs that being with /api/map must include a valid API key in the `Authorization` header. The format is:
Authorization: Bearer <YOUR_MAP_API_KEY>
If the API key is missing or incorrect, youll receive a `401 Unauthorized` response.
No api key is required for routes that being with /api/common
---
## Endpoints Overview
### 1. List Systems
GET /api/map/systems?map_id=<UUID>
GET /api/map/systems?slug=<map-slug>
- **Description:** Retrieves a list of systems associated with the specified map (by `map_id` or `slug`).
- **Authentication:** Required via `Authorization` header.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
- `all=true` (optional) — if set, returns *all* systems instead of only "visible" systems.
#### Example Request
```
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/systems?slug=some-slug"
```
#### Example Response
```
{
"data": [
{
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"status": 0,
"tag": null,
"visible": false,
"description": null,
"labels": "<REDACTED_JSON>",
"inserted_at": "2025-01-01T13:38:42.875843Z",
"updated_at": "2025-01-01T13:40:16.750234Z",
"locked": false,
"solar_system_id": <REDACTED_NUMBER>,
"map_id": "<REDACTED_ID>",
"custom_name": null,
"position_x": 1125,
"position_y": -285
},
...
]
}
```
---
### 2. Show Single System
GET /api/map/system?id=<SOLAR_SYSTEM_ID>&map_id=<UUID>
GET /api/map/system?id=<SOLAR_SYSTEM_ID>&slug=<map-slug>
- **Description:** Retrieves information for a specific system on the specified map. You must provide:
- `id` (the `solar_system_id`).
- Either `map_id` or `slug`.
- **Authentication:** Required via `Authorization` header.
#### Example Request
```
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/system?id=<REDACTED_NUMBER>&slug=<REDACTED_SLUG>"
```
#### Example Response
```
{
"data": {
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"status": 0,
"tag": null,
"visible": false,
"description": null,
"labels": "<REDACTED_JSON>",
"inserted_at": "2025-01-03T06:30:02.069090Z",
"updated_at": "2025-01-03T07:47:07.471051Z",
"locked": false,
"solar_system_id": <REDACTED_NUMBER>,
"map_id": "<REDACTED_ID>",
"custom_name": null,
"position_x": 1005,
"position_y": 765
}
}
```
---
### 2. Show Single System Static Info
GET /api/common/static-system-info?id=<SOLAR_SYSTEM_ID>
- **Description:** Retrieves the static information for a specific system.
- **Authentication:** No API token required
#### Example Request
```
curl "https://wanderer.example.com/api/common/static-system-info?id=31002229
```
#### Example Response
```
{
"data": {
"solar_system_id": 31002229,
"triglavian_invasion_status": "Normal",
"solar_system_name": "J132946",
"system_class": 5,
"region_id": 11000028,
"constellation_id": 21000278,
"solar_system_name_lc": "j132946",
"constellation_name": "E-C00278",
"region_name": "E-R00028",
"security": "-1.0",
"type_description": "Class 5",
"class_title": "C5",
"is_shattered": false,
"effect_name": null,
"effect_power": 5,
"statics": [
"H296"
],
"wandering": [
"D792",
"C140",
"Z142"
],
"sun_type_id": 38
}
}
```
---
### 3. List Tracked Characters
GET /api/map/characters?map_id=<UUID>
GET /api/map/characters?slug=<map-slug>
- **Description:** Retrieves a list of tracked characters for the specified map (by `map_id` or `slug`), including metadata such as corporation/alliance details.
- **Authentication:** Required via `Authorization` header.
#### Example Request
```
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/map/characters?slug=some-slug"
```
#### Example Response
```
{
"data": [
{
"id": "<REDACTED_ID>",
"character": {
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"inserted_at": "2025-01-01T05:24:18.461721Z",
"updated_at": "2025-01-03T07:45:52.294052Z",
"alliance_id": "<REDACTED>",
"alliance_name": "<REDACTED>",
"alliance_ticker": "<REDACTED>",
"corporation_id": "<REDACTED>",
"corporation_name": "<REDACTED>",
"corporation_ticker": "<REDACTED>",
"eve_id": "<REDACTED>"
},
"tracked": true,
"map_id": "<REDACTED_ID>"
},
...
]
}
```
---
## Conclusion
Using these APIs, you can programmatically retrieve system and character information from your map. Whether youre building a custom analytics dashboard, a corp management tool, or just want to explore data outside the standard UI, these endpoints provide a straightforward way to fetch up-to-date map details.
For questions or additional support, please reach out to the Wanderer Team.
Fly safe,
WANDERER TEAM

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.MigrateResources1 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_character_settings_v1) do
add :followed, :boolean, default: false
end
end
def down do
alter table(:map_character_settings_v1) do
remove :followed
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddTemporaryName do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_system_v1) do
add :temporary_name, :text
end
end
def down do
alter table(:map_system_v1) do
remove :temporary_name
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddPublicApiKey do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:maps_v1) do
add :public_api_key, :text
end
end
def down do
alter table(:maps_v1) do
remove :public_api_key
end
end
end