Compare commits

..

1 Commits

Author SHA1 Message Date
achichenkov
ef26d7129a fix(Map): Fix icons of main, follow and shattered 2025-04-13 17:08:15 +03:00
322 changed files with 6684 additions and 23841 deletions

View File

@@ -8,5 +8,4 @@ export GIT_SHA="1111"
export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false"
export WANDERER_CHARACTER_API_DISABLED="false"
export WANDERER_KILLS_SERVICE_ENABLED="true"
export WANDERER_KILLS_BASE_URL="ws://host.docker.internal:4004"
export WANDERER_ZKILL_PRELOAD_DISABLED="false"

View File

@@ -18,8 +18,49 @@ permissions:
contents: write
jobs:
deploy-test:
name: 🚀 Deploy to test env (fly.io)
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' || (github.ref == 'refs/heads/main' && github.event_name == 'push') }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: 👀 Read app name
uses: SebRollen/toml-action@v1.0.0
id: app_name
with:
file: "fly.toml"
field: "app"
- name: 🚀 Deploy Test
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
manual-approval:
name: Manual Approval
runs-on: ubuntu-latest
needs: deploy-test
if: success()
permissions:
issues: write
steps:
- name: Await Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: DmitryPopov
minimum-approvals: 1
issue-title: "Manual Approval Required for Release"
issue-body: "Please approve or deny the deployment."
build:
name: 🛠 Build
needs: manual-approval
runs-on: ubuntu-22.04
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
permissions:

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,5 @@ module.exports = {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
"linebreak-style": "off",
},
};

View File

@@ -7,5 +7,5 @@
"semi": true,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "auto"
"endOfLine": "lf"
}

View File

@@ -1,14 +1,14 @@
import { PrimeReactProvider } from 'primereact/api';
import { ErrorBoundary } from 'react-error-boundary';
import { PrimeReactProvider } from 'primereact/api';
import { ReactFlowProvider } from 'reactflow';
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
import { ErrorInfo, useCallback, useEffect, useRef } from 'react';
import { ReactFlowProvider } from 'reactflow';
import { useMapperHandlers } from './useMapperHandlers';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import './common-styles/main.scss';
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
const ErrorFallback = () => {
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
@@ -20,7 +20,7 @@ export default function MapRoot({ hooks }) {
const mapperHandlerRefs = useRef([providerRef]);
const { handleCommand, handleMapEvent } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
const { handleCommand, handleMapEvent, handleMapEvents } = useMapperHandlers(mapperHandlerRefs.current, hooksRef);
const logError = useCallback((error: Error, info: ErrorInfo) => {
if (!hooksRef.current) {
@@ -35,6 +35,7 @@ export default function MapRoot({ hooks }) {
}
hooksRef.current.handleEvent('map_event', handleMapEvent);
hooksRef.current.handleEvent('map_events', handleMapEvents);
}, []);
return (

View File

@@ -99,11 +99,6 @@
.p-dropdown-item {
padding: 0.25rem 0.5rem;
font-size: 14px;
width: 100%;
.p-dropdown-item-label {
width: 100%;
}
}
.p-dropdown-item-group {
@@ -185,30 +180,3 @@
.p-datatable .p-datatable-tbody > tr.p-highlight {
background: initial;
}
.suppress-menu-behaviour {
pointer-events: none;
.p-menuitem-content {
pointer-events: initial;
background-color: initial !important;
}
.p-menuitem-content:hover {
background-color: initial !important;
}
}
.p-autocomplete .p-autocomplete-multiple-container:not(.p-disabled).p-focus {
box-shadow: 0 0 0 1px #335c7e;
border-color: #335c7e;
}
.p-inputtext:enabled:focus {
box-shadow: 0 0 0 1px #335c7e;
border-color: #335c7e;
}
.p-inputtext:enabled:hover {
border-color: #335c7e;
}

View File

@@ -1,13 +1,14 @@
import { emitMapEvent } from '@/hooks/Mapper/events';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { useCallback } from 'react';
import clsx from 'clsx';
import { useAutoAnimate } from '@formkit/auto-animate/react';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './Characters.module.scss';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { PrimeIcons } from 'primereact/api';
interface CharactersProps {
data: CharacterTypeRaw[];
}
@@ -16,22 +17,13 @@ export const Characters = ({ data }: CharactersProps) => {
const [parent] = useAutoAnimate();
const {
outCommand,
data: { mainCharacterEveId, followingCharacterEveId },
} = useMapRootState();
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
if (!character) {
return;
}
await outCommand({
type: OutCommand.startTracking,
data: { character_eve_id: character.eve_id },
});
const handleSelect = useCallback((character: CharacterTypeRaw) => {
emitMapEvent({
name: Commands.centerSystem,
data: character.location?.solar_system_id?.toString(),
data: character?.location?.solar_system_id?.toString(),
});
}, []);
@@ -45,26 +37,14 @@ export const Characters = ({ data }: CharactersProps) => {
className={clsx(
'overflow-hidden relative',
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
'transition-colors duration-250 hover:bg-stone-300/90',
'transition-colors duration-250',
{
['border-stone-800/90']: !character.online,
['border-lime-600/70']: character.online,
},
)}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
title={character.name}
>
{character.tracking_paused && (
<>
<span
className={clsx(
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
'pi',
PrimeIcons.PAUSE,
)}
/>
</>
)}
{mainCharacterEveId === character.eve_id && (
<span
className={clsx(
@@ -75,7 +55,6 @@ export const Characters = ({ data }: CharactersProps) => {
)}
/>
)}
{followingCharacterEveId === character.eve_id && (
<span
className={clsx(

View File

@@ -1,12 +1,11 @@
import React, { RefObject } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { PingType, SolarSystemRawType } from '@/hooks/Mapper/types';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { useContextMenuSystemItems } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/useContextMenuSystemItems.tsx';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
export interface ContextMenuSystemProps {
hubs: string[];
userHubs: string[];
contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined;
systems: SolarSystemRawType[];
@@ -14,12 +13,10 @@ export interface ContextMenuSystemProps {
onLockToggle(): void;
onOpenSettings(): void;
onHubToggle(): void;
onUserHubToggle(): void;
onSystemTag(val?: string): void;
onSystemStatus(val: number): void;
onSystemLabels(val: string): void;
onCustomLabelDialog(): void;
onTogglePing(type: PingType, solar_system_id: string, hasPing: boolean): void;
onWaypointSet: WaypointSetContextHandler;
}
@@ -28,7 +25,7 @@ export const ContextMenuSystem: React.FC<ContextMenuSystemProps> = ({ contextMen
return (
<>
<ContextMenu className="min-w-[200px]" model={items} ref={contextMenuRef} breakpoint="767px" />
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
</>
);
};

View File

@@ -1,4 +1,3 @@
export * from './useTagMenu';
export * from './useStatusMenu';
export * from './useLabelsMenu';
export * from './useUserRoute';

View File

@@ -1 +1 @@
export * from './useTagMenu.tsx';
export * from './useTagMenu.ts';

View File

@@ -0,0 +1,68 @@
import { MenuItem } from 'primereact/menuitem';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useRef } from 'react';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
const AVAILABLE_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F', 'X', 'Y', 'Z'];
const AVAILABLE_NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
export const useTagMenu = (
systems: SolarSystemRawType[],
systemId: string | undefined,
onSystemTag: (val?: string) => void,
): (() => MenuItem) => {
const ref = useRef({ onSystemTag, systems, systemId });
ref.current = { onSystemTag, systems, systemId };
return useCallback(() => {
const { onSystemTag, systemId, systems } = ref.current;
const system = systemId ? getSystemById(systems, systemId) : undefined;
const isSelectedLetters = AVAILABLE_LETTERS.includes(system?.tag ?? '');
const isSelectedNumbers = AVAILABLE_NUMBERS.includes(system?.tag ?? '');
const menuItem: MenuItem = {
label: 'Tag',
icon: PrimeIcons.HASHTAG,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedLetters || isSelectedNumbers }),
items: [
...(system?.tag !== '' && system?.tag !== null
? [
{
label: 'Clear',
icon: PrimeIcons.BAN,
command: () => onSystemTag(),
},
]
: []),
{
label: 'Letter',
icon: PrimeIcons.TAGS,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedLetters }),
items: AVAILABLE_LETTERS.map(x => ({
label: x,
icon: PrimeIcons.TAG,
command: () => onSystemTag(x),
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: system?.tag === x }),
})),
},
{
label: 'Digit',
icon: PrimeIcons.TAGS,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedNumbers }),
items: AVAILABLE_NUMBERS.map(x => ({
label: x,
icon: PrimeIcons.TAG,
command: () => onSystemTag(x),
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: system?.tag === x }),
})),
},
],
};
return menuItem;
}, []);
};

View File

@@ -1,95 +0,0 @@
import { MenuItem } from 'primereact/menuitem';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useRef } from 'react';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { getSystemById } from '@/hooks/Mapper/helpers';
import clsx from 'clsx';
import { GRADIENT_MENU_ACTIVE_CLASSES } from '@/hooks/Mapper/constants.ts';
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
import { Button } from 'primereact/button';
const AVAILABLE_TAGS = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'X',
'Y',
'Z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
];
export const useTagMenu = (
systems: SolarSystemRawType[],
systemId: string | undefined,
onSystemTag: (val?: string) => void,
): (() => MenuItem) => {
const ref = useRef({ onSystemTag, systems, systemId });
ref.current = { onSystemTag, systems, systemId };
return useCallback(() => {
const { onSystemTag, systemId, systems } = ref.current;
const system = systemId ? getSystemById(systems, systemId) : undefined;
const isSelectedTag = AVAILABLE_TAGS.includes(system?.tag ?? '');
const menuItem: MenuItem = {
label: 'Tag',
icon: PrimeIcons.HASHTAG,
className: clsx({ [GRADIENT_MENU_ACTIVE_CLASSES]: isSelectedTag }),
items: [
{
label: 'Digit',
icon: PrimeIcons.TAGS,
className: '!h-[128px] suppress-menu-behaviour',
template: () => {
return (
<LayoutEventBlocker className="flex flex-col gap-1 w-[200px] h-full px-2">
<div className="grid grid-cols-[auto_auto_auto_auto_auto_auto] gap-1">
{AVAILABLE_TAGS.map(x => (
<Button
outlined={system?.tag !== x}
severity="warning"
key={x}
value={x}
size="small"
className="p-[3px] justify-center"
onClick={() => system?.tag !== x && onSystemTag(x)}
>
{x}
</Button>
))}
<Button
disabled={!isSelectedTag}
icon="pi pi-ban"
size="small"
className="!p-0 !w-[initial] justify-center"
outlined
severity="help"
onClick={() => onSystemTag()}
></Button>
</div>
</LayoutEventBlocker>
);
},
},
],
};
return menuItem;
}, []);
};

View File

@@ -1,42 +0,0 @@
import { MapUserAddIcon, MapUserDeleteIcon } from '@/hooks/Mapper/icons';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef } from 'react';
import { WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
interface UseUserRouteProps {
systemId: string | undefined;
userHubs: string[];
onUserHubToggle(): void;
}
export const useUserRoute = ({ userHubs, systemId, onUserHubToggle }: UseUserRouteProps) => {
const {
data: { isSubscriptionActive },
windowsSettings,
} = useMapRootState();
const ref = useRef({ userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings });
ref.current = { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings };
return useCallback(() => {
const { userHubs, systemId, onUserHubToggle, isSubscriptionActive, windowsSettings } = ref.current;
const isVisibleUserRoutes = windowsSettings.visible.some(x => x === WidgetsIds.userRoutes);
if (!isSubscriptionActive || !isVisibleUserRoutes || !systemId) {
return [];
}
return [
{
label: !userHubs.includes(systemId) ? 'Add User Route' : 'Remove User Route',
icon: !userHubs.includes(systemId) ? (
<MapUserAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapUserDeleteIcon className="mr-1 relative left-[-2px]" />
),
command: onUserHubToggle,
},
];
}, [windowsSettings]);
};

View File

@@ -5,29 +5,22 @@ import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
// import { PingType } from '@/hooks/Mapper/types/ping.ts';
interface UseContextMenuSystemHandlersProps {
hubs: string[];
userHubs: string[];
systems: SolarSystemRawType[];
outCommand: OutCommandHandler;
}
export const useContextMenuSystemHandlers = ({
systems,
hubs,
userHubs,
outCommand,
}: UseContextMenuSystemHandlersProps) => {
export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>();
const { deleteSystems } = useDeleteSystems();
const ref = useRef({ hubs, userHubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, userHubs, system, systems, outCommand, deleteSystems };
const ref = useRef({ hubs, system, systems, outCommand, deleteSystems });
ref.current = { hubs, system, systems, outCommand, deleteSystems };
const open = useCallback((ev: any, systemId: string) => {
setSystem(systemId);
@@ -79,37 +72,6 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined);
}, []);
const onUserHubToggle = useCallback(() => {
const { userHubs, system, outCommand } = ref.current;
if (!system) {
return;
}
outCommand({
type: !userHubs.includes(system) ? OutCommand.addUserHub : OutCommand.deleteUserHub,
data: {
system_id: system,
},
});
setSystem(undefined);
}, []);
// const onTogglePingRally = useCallback(() => {
// const { userHubs, system, outCommand } = ref.current;
// if (!system) {
// return;
// }
//
// outCommand({
// type: OutCommand.openPing,
// data: {
// solar_system_id: system,
// type: PingType.Rally,
// },
// });
// setSystem(undefined);
// }, []);
const onSystemTag = useCallback((tag?: string) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -142,6 +104,7 @@ export const useContextMenuSystemHandlers = ({
setSystem(undefined);
}, []);
const onSystemStatus = useCallback((status: number) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -214,8 +177,6 @@ export const useContextMenuSystemHandlers = ({
onDeleteSystem,
onLockToggle,
onHubToggle,
onUserHubToggle,
// onTogglePingRally,
onSystemTag,
onSystemTemporaryName,
onSystemStatus,

View File

@@ -1,9 +1,4 @@
import {
useLabelsMenu,
useStatusMenu,
useTagMenu,
useUserRoute,
} from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
import { useLabelsMenu, useStatusMenu, useTagMenu } from '@/hooks/Mapper/components/contexts/ContextMenuSystem/hooks';
import { useMemo } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers';
import classes from './ContextMenuSystem.module.scss';
@@ -15,19 +10,11 @@ import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
import { PingType } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import clsx from 'clsx';
import { MenuItem } from 'primereact/menuitem';
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
export const useContextMenuSystemItems = ({
onDeleteSystem,
onLockToggle,
onHubToggle,
onUserHubToggle,
onTogglePing,
onSystemTag,
onSystemStatus,
onSystemLabels,
@@ -36,7 +23,6 @@ export const useContextMenuSystemItems = ({
onWaypointSet,
systemId,
hubs,
userHubs,
systems,
}: Omit<ContextMenuSystemProps, 'contextMenuRef'>) => {
const getTags = useTagMenu(systems, systemId, onSystemTag);
@@ -44,33 +30,11 @@ export const useContextMenuSystemItems = ({
const getLabels = useLabelsMenu(systems, systemId, onSystemLabels, onCustomLabelDialog);
const getWaypointMenu = useWaypointMenu(onWaypointSet);
const canLockSystem = useMapCheckPermissions([UserPermission.LOCK_SYSTEM]);
const canManageSystem = useMapCheckPermissions([UserPermission.UPDATE_SYSTEM]);
const canDeleteSystem = useMapCheckPermissions([UserPermission.DELETE_SYSTEM]);
const getUserRoutes = useUserRoute({ userHubs, systemId, onUserHubToggle });
const {
data: { pings, isSubscriptionActive },
} = useMapRootState();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const isShowPingBtn = useMemo(() => {
if (!isSubscriptionActive) {
return false;
}
if (pings.length === 0) {
return true;
}
return pings[0].solar_system_id === systemId;
}, [isSubscriptionActive, pings, systemId]);
return useMemo((): MenuItem[] => {
return useMemo(() => {
const system = systemId ? getSystemById(systems, systemId) : undefined;
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const hasPing = ping?.solar_system_id === systemId;
if (!system || !systemId) {
return [];
}
@@ -97,104 +61,50 @@ export const useContextMenuSystemItems = ({
...getLabels(),
...getWaypointMenu(systemId, systemStaticInfo.system_class),
{
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
icon: !hubs.includes(systemId) ? (
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: PrimeIcons.MAP_MARKER,
command: onHubToggle,
},
...getUserRoutes(),
{ separator: true },
{
command: () => onTogglePing(PingType.Rally, systemId, hasPing),
disabled: !isShowPingBtn,
template: () => {
const iconClasses = clsx({
'pi text-cyan-400 hero-signal': !hasPing,
'pi text-red-400 hero-signal-slash': hasPing,
});
if (isShowPingBtn) {
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Locked. Ping can be set only for one system."
infoClass="pi-lock text-stone-500 mr-[12px]"
>
<WdMenuItem disabled icon={iconClasses}>
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
...(system.locked && canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []),
...(!system.locked && canManageSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
...(canDeleteSystem && !system.locked
? [
...(system.locked
? canLockSystem
? [
{
label: 'Unlock',
icon: PrimeIcons.LOCK_OPEN,
command: onLockToggle,
},
]
: []
: [
...(canLockSystem
? [
{
label: 'Lock',
icon: PrimeIcons.LOCK,
command: onLockToggle,
},
]
: []),
{ separator: true },
{
label: 'Delete',
icon: PrimeIcons.TRASH,
command: onDeleteSystem,
disabled: hasPing,
template: () => {
if (!hasPing) {
return <WdMenuItem icon="text-red-400 pi pi-trash">Delete</WdMenuItem>;
}
return (
<MenuItemWithInfo
infoTitle="Locked. System can not be deleted until ping set."
infoClass="pi-lock text-stone-500 mr-[12px]"
>
<WdMenuItem disabled icon="text-red-400 pi pi-trash">
Delete
</WdMenuItem>
</MenuItemWithInfo>
);
},
},
]
: []),
]),
];
}, [
systemId,
canLockSystem,
systems,
systemId,
getTags,
getStatus,
getLabels,
getWaypointMenu,
getUserRoutes,
hubs,
onHubToggle,
canLockSystem,
onLockToggle,
canDeleteSystem,
onDeleteSystem,
onOpenSettings,
onTogglePing,
ping,
isShowPingBtn,
onLockToggle,
onDeleteSystem,
]);
};

View File

@@ -11,7 +11,6 @@ import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { Route } from '@/hooks/Mapper/types/routes.ts';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
@@ -70,12 +69,8 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class),
{
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
icon: !hubs.includes(systemId) ? (
<MapAddIcon className="mr-1 relative left-[-2px]" />
) : (
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
),
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
icon: PrimeIcons.MAP_MARKER,
command: onHubToggle,
},
...(!systemOnMap

View File

@@ -1,25 +1,25 @@
import * as React from 'react';
import { useCallback, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { Commands, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
export const useContextMenuSystemInfoHandlers = () => {
const { outCommand } = useMapRootState();
const { hubs = [], toggleHubCommand } = useRouteProvider();
interface UseContextMenuSystemHandlersProps {
hubs: string[];
outCommand: OutCommandHandler;
}
export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand }: UseContextMenuSystemHandlersProps) => {
const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>();
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
const ref = useRef({ hubs, system, outCommand, toggleHubCommand });
ref.current = { hubs, system, outCommand, toggleHubCommand };
const ref = useRef({ hubs, system, outCommand });
ref.current = { hubs, system, outCommand };
const open = useCallback(
(ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
@@ -33,12 +33,17 @@ export const useContextMenuSystemInfoHandlers = () => {
);
const onHubToggle = useCallback(() => {
const { system } = ref.current;
const { hubs, system, outCommand } = ref.current;
if (!system) {
return;
}
ref.current.toggleHubCommand(system);
outCommand({
type: !hubs.includes(system) ? OutCommand.addHub : OutCommand.deleteHub,
data: {
system_id: system,
},
});
setSystem(undefined);
}, []);
@@ -54,8 +59,6 @@ export const useContextMenuSystemInfoHandlers = () => {
system_id: solarSystemId,
},
});
// TODO add it to some queue
setTimeout(() => {
emitMapEvent({
name: Commands.centerSystem,

View File

@@ -1,6 +1,6 @@
import { useCallback, useRef } from 'react';
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { LayoutEventBlocker, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons.ts';
import classes from './FastSystemActions.module.scss';
import clsx from 'clsx';
@@ -59,21 +59,9 @@ export const FastSystemActions = ({
return (
<LayoutEventBlocker className={clsx('flex px-2 gap-2 justify-between items-center h-full')}>
<div className={clsx('flex gap-2 items-center h-full', classes.Links)}>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Anoikis' }}
source={ANOIK_ICON}
onClick={handleOpenAnoikis}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Dotlan' }}
source={DOTLAN_ICON}
onClick={handleOpenDotlan}
/>
<WdImgButton tooltip={{ content: 'Open zkillboard' }} source={ZKB_ICON} onClick={handleOpenZKB} />
<WdImgButton tooltip={{ content: 'Open Anoikis' }} source={ANOIK_ICON} onClick={handleOpenAnoikis} />
<WdImgButton tooltip={{ content: 'Open Dotlan' }} source={DOTLAN_ICON} onClick={handleOpenDotlan} />
</div>
<div className="flex gap-2 items-center pl-1">
@@ -81,14 +69,14 @@ export const FastSystemActions = ({
textSize={WdImageSize.off}
className={PrimeIcons.COPY}
onClick={copySystemNameToClipboard}
tooltip={{ position: TooltipPosition.top, content: 'Copy system name' }}
tooltip={{ content: 'Copy system name' }}
/>
{showEdit && (
<WdImgButton
textSize={WdImageSize.off}
className="pi pi-pen-to-square text-base"
onClick={onOpenSettings}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
tooltip={{ content: 'Edit system name and description' }}
/>
)}
</div>

View File

@@ -1,6 +1,6 @@
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { getSystemStaticInfo } from '../../mapRootProvider/hooks/useLoadSystemStatic';
interface UseSystemInfoProps {
@@ -17,7 +17,7 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
const dynamicInfo = getSystemById(systems, systemId);
if (!staticInfo || !dynamicInfo) {
return { dynamicInfo, staticInfo, leadsTo: [] };
throw new Error(`Error on getting system ${systemId}`);
}
const leadsTo = connections

View File

@@ -28,12 +28,11 @@ import {
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
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';
import type { PanelPosition } from '@reactflow/core';
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
@@ -96,8 +95,6 @@ interface MapCompProps {
isShowBackgroundPattern?: boolean;
isSoftBackground?: boolean;
theme?: string;
pings: PingData[];
minimapPlacement?: PanelPosition;
}
const MapComp = ({
@@ -115,8 +112,6 @@ const MapComp = ({
isSoftBackground,
theme,
onAddSystem,
pings,
minimapPlacement = 'bottom-right',
}: MapCompProps) => {
const { getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
@@ -211,9 +206,8 @@ const MapComp = ({
...x,
showKSpaceBG: showKSpaceBG,
isThickConnections: isThickConnections,
pings,
}));
}, [showKSpaceBG, isThickConnections, pings, update]);
}, [showKSpaceBG, isThickConnections, update]);
return (
<>
@@ -276,9 +270,7 @@ const MapComp = ({
// onlyRenderVisibleElements
selectionMode={SelectionMode.Partial}
>
{isShowMinimap && (
<MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} position={minimapPlacement} />
)}
{isShowMinimap && <MiniMap pannable zoomable ariaLabel="Mini map" className={minimapClasses} />}
{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}>

View File

@@ -38,10 +38,6 @@ const INITIAL_DATA: MapData = {
systemSignatures: {} as Record<string, SystemSignature[]>,
options: {} as Record<string, string | boolean>,
isSubscriptionActive: false,
mainCharacterEveId: null,
followingCharacterEveId: null,
userHubs: [],
pings: [],
};
export interface MapContextProps {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import classes from './SolarSystemEdge.module.scss';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, Position, useStore } from 'reactflow';
import { EdgeLabelRenderer, EdgeProps, getBezierPath, getSmoothStepPath, Position, useStore } from 'reactflow';
import { getEdgeParams } from '@/hooks/Mapper/components/map/utils.ts';
import clsx from 'clsx';
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
@@ -51,11 +51,11 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
const [hovered, setHovered] = useState(false);
const [path, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos] = useMemo(() => {
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode!, targetNode!);
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
const method = isWormhole ? getBezierPath : getBezierPath;
const method = isWormhole ? getBezierPath : getSmoothStepPath;
const [edgePath, labelX, labelY] = method({
sourceX: sx - offset.x,

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useKillsCounter } from '../../hooks/useKillsCounter.ts';
import { useKillsCounter } from '../../hooks/useKillsCounter';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
import {
KILLS_ROW_HEIGHT,
SystemKillsList,
@@ -26,7 +26,11 @@ export const KillsCounter = ({
children,
size = TooltipSize.xs,
}: KillsBookmarkTooltipProps) => {
const { isLoading, kills: detailedKills } = useKillsCounter({
const {
isLoading,
kills: detailedKills,
systemNameMap,
} = useKillsCounter({
realSystemId: systemId,
});
@@ -49,7 +53,7 @@ export const KillsCounter = ({
content={
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
<div className="flex-1 h-full">
<SystemKillsList kills={limitedKills} onlyOneSystem timeRange={1} />
<SystemKillsList kills={limitedKills} onlyOneSystem />
</div>
</div>
}

View File

@@ -3,11 +3,11 @@ import clsx from 'clsx';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
import { CharItemProps, LocalCharactersList } from '../../../mapInterface/widgets/LocalCharacters/components';
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters.tsx';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings.ts';
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
import classes from './SolarSystemLocalCounter.module.scss';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider';
import { useTheme } from '@/hooks/Mapper/hooks/useTheme.ts';
import { AvailableThemes } from '@/hooks/Mapper/mapRootProvider/types.ts';
import classes from './LocalCounter.module.scss';
interface LocalCounterProps {
localCounterCharacters: Array<CharItemProps>;

View File

@@ -1,23 +1,11 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
$pastel-pink: #d291bc;
$dark-bg: #2d2d2d;
$text-color: #ffffff;
$tooltip-bg: #202020;
$neon-color-1: rgb(27, 132, 236);
$neon-color-3: rgba(27, 132, 236, 0.40);
@keyframes move-stripes {
from {
background-position: 0 0;
}
to {
background-position: 30px 0;
}
}
.RootCustomNode {
display: flex;
width: 130px;
@@ -40,12 +28,11 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
z-index: 3;
overflow: hidden;
&.Pochven,
&.Mataria,
&.Amarria,
&.Gallente,
&.Caldaria {
&::after {
&::before {
content: '';
position: absolute;
top: 0;
@@ -61,7 +48,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Mataria {
&::after {
&::before {
background-image: url('/images/mataria-180.png');
opacity: 0.6;
background-position-x: 1px;
@@ -70,7 +57,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Caldaria {
&::after {
&::before {
background-image: url('/images/caldaria-180.png');
opacity: 0.6;
background-position-x: 1px;
@@ -79,7 +66,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Amarria {
&::after {
&::before {
opacity: 0.45;
background-image: url('/images/amarr-180.png');
background-position-x: 0;
@@ -88,7 +75,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
&.Gallente {
&::after {
&::before {
opacity: 0.5;
background-image: url('/images/gallente-180.png');
background-position-x: 1px;
@@ -96,43 +83,11 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
}
}
&.Pochven {
&::after {
opacity: 0.8;
background-image: url('/images/pochven.webp');
background-position-x: 0;
background-position-y: -13px;
}
}
&.selected {
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
&.rally {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
$neon-color-3 0px,
$neon-color-3 8px,
transparent 8px,
transparent 21px
);
background-size: 30px 30px;
animation: move-stripes 3s linear infinite;
}
}
&.eve-system-status-home {
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
background-image: linear-gradient(45deg, var(--eve-solar-system-status-color-background), transparent);

View File

@@ -12,24 +12,26 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { Tag } from 'primereact/tag';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
// let render = 0;
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
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, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
@@ -38,7 +40,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
)}
{localKillsCount != null && localKillsCount > 0 && nodeVars.solarSystemId && (
{localKillsCount && localKillsCount > 0 && nodeVars.solarSystemId && (
<KillsCounter
killsCount={localKillsCount}
systemId={nodeVars.solarSystemId}
@@ -48,17 +50,11 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{localKillsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</KillsCounter>
)}
{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.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
@@ -71,11 +67,8 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
className={clsx(
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined && classes[STATUS_CLASSES[nodeVars.status]],
{
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
@@ -93,11 +86,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
</div>
{nodeVars.tag != null && nodeVars.tag !== '' && (
<Tag
value={nodeVars.tag}
severity="warning"
className="py-0 px-[2px] text-[9px] [&_.p-tag-value]:leading-[1.3]"
></Tag>
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
)}
<div

View File

@@ -12,23 +12,26 @@ import {
} from '@/hooks/Mapper/components/map/constants';
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
import { LocalCounter } from './SolarSystemLocalCounter';
import { KillsCounter } from './SolarSystemKillsCounter';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCounter';
// let render = 0;
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const localKillsCount = useNodeKillsCount(nodeVars.solarSystemId, nodeVars.killsCount);
// console.log('JOipP', `render ${nodeVars.id}`, render++);
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, '!pr-[2px]')}>
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
@@ -47,17 +50,11 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
>
<div className={clsx(classes.BookmarkWithIcon)}>
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
<span className={clsx(classes.text)}>{localKillsCount}</span>
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
</div>
</KillsCounter>
)}
{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.labelsInfo.map(x => (
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
{x.shortName}
@@ -71,10 +68,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
classes.RootCustomNode,
nodeVars.regionClass && classes[nodeVars.regionClass],
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
{
[classes.selected]: nodeVars.selected,
[classes.rally]: nodeVars.isRally,
},
{ [classes.selected]: nodeVars.selected },
)}
onMouseDownCapture={e => nodeVars.dbClick(e)}
>
@@ -119,13 +113,23 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
{nodeVars.customName && (
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
<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="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
<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>
)}

View File

@@ -16,7 +16,7 @@ export enum SOLAR_SYSTEM_CLASS_IDS {
thera = 12,
c13 = 13,
sentinel = 14,
barbican = 15,
baribican = 15,
vidette = 16,
conflux = 17,
redoubt = 18,
@@ -82,7 +82,7 @@ export const SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS = {
thera: SOLAR_SYSTEM_CLASS_GROUPS.thera,
c13: SOLAR_SYSTEM_CLASS_GROUPS.c13,
sentinel: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
barbican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
baribican: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
vidette: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
conflux: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
redoubt: SOLAR_SYSTEM_CLASS_GROUPS.drifter,
@@ -217,7 +217,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 14,
effectPower: 2,
title: 'Class 14 (Sentinel Drifter)',
shortTitle: 'Sentinel MZ',
shortTitle: 'Sentinel',
},
{
id: 'barbican',
@@ -225,7 +225,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 15,
effectPower: 2,
title: 'Class 15 (Barbican Drifter)',
shortTitle: 'Liberated Barbican',
shortTitle: 'Barbican',
},
{
id: 'vidette',
@@ -233,7 +233,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 16,
effectPower: 2,
title: 'Class 16 (Vidette Drifter)',
shortTitle: 'Sanctified Vidette',
shortTitle: 'Vidette',
},
{
id: 'conflux',
@@ -241,7 +241,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 17,
effectPower: 2,
title: 'Class 17 (Conflux Drifter)',
shortTitle: 'Conflux Eyrie',
shortTitle: 'Conflux',
},
{
id: 'redoubt',
@@ -249,7 +249,7 @@ export const WORMHOLES_ADDITIONAL_INFO_RAW: WormholesAdditionalInfoType[] = [
wormholeClassID: 18,
effectPower: 2,
title: 'Class 18 (Redoubt Drifter)',
shortTitle: 'Azdaja Redoubt',
shortTitle: 'Redoubt',
},
{
id: 'a1',

View File

@@ -10,7 +10,7 @@ export const isWormholeSpace = (wormholeClassID: number) => {
case SOLAR_SYSTEM_CLASS_IDS.c6:
case SOLAR_SYSTEM_CLASS_IDS.c13:
case SOLAR_SYSTEM_CLASS_IDS.thera:
case SOLAR_SYSTEM_CLASS_IDS.barbican:
case SOLAR_SYSTEM_CLASS_IDS.baribican:
case SOLAR_SYSTEM_CLASS_IDS.vidette:
case SOLAR_SYSTEM_CLASS_IDS.conflux:
case SOLAR_SYSTEM_CLASS_IDS.redoubt:

View File

@@ -1,4 +1,5 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
@@ -6,7 +7,6 @@ import {
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => {
const { update } = useMapState();

View File

@@ -1,6 +1,6 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
export const useMapCommands = () => {
const { update } = useMapState();
@@ -8,21 +8,13 @@ export const useMapCommands = () => {
const ref = useRef({ update });
ref.current = { update };
const mapUpdated = useCallback(({ hubs, system_signatures, kills }: CommandMapUpdated) => {
const mapUpdated = useCallback(({ hubs }: CommandMapUpdated) => {
const out: Partial<MapData> = {};
if (hubs) {
out.hubs = hubs;
}
if (system_signatures) {
out.systemSignatures = system_signatures;
}
if (kills) {
out.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
}
ref.current.update(out);
}, []);

View File

@@ -1,8 +1,8 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useCallback, useRef } from 'react';
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapInit = () => {
const rf = useReactFlow();

View File

@@ -22,7 +22,6 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
systemId: realSystemId,
outCommand,
showAllVisible: false,
sinceHours: 1,
});
const filteredKills = useMemo(() => {

View File

@@ -115,18 +115,35 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
}, 500);
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
// do nothing here
break;
case Commands.signaturesUpdated:
// do nothing here
break;
case Commands.linkSignatureToSystem:
// do nothing here
break;
case Commands.detailedKillsUpdated:
// do nothing here
break;
case Commands.characterActivityData:
break;
case Commands.trackingCharactersData:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
// do nothing
break;
default:

View File

@@ -1,7 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
interface Kill {
solar_system_id: number | string;
@@ -10,66 +9,32 @@ interface Kill {
interface MapEvent {
name: Commands;
data?: unknown;
data?: any;
payload?: Kill[];
}
export function useNodeKillsCount(systemId: number | string, initialKillsCount: number | null): number | null {
export function useNodeKillsCount(
systemId: number | string,
initialKillsCount: number | null
): number | null {
const [killsCount, setKillsCount] = useState<number | null>(initialKillsCount);
const { data: mapData } = useMapRootState();
const { detailedKills = {} } = mapData;
// Calculate 1-hour kill count from detailed kills
const oneHourKillCount = useMemo(() => {
const systemKills = detailedKills[systemId] || [];
// If we have detailed kills data (even if empty), use it for counting
if (Object.prototype.hasOwnProperty.call(detailedKills, systemId)) {
const oneHourAgo = Date.now() - 60 * 60 * 1000; // 1 hour in milliseconds
const recentKills = systemKills.filter(kill => {
if (!kill.kill_time) return false;
const killTime = new Date(kill.kill_time).getTime();
if (isNaN(killTime)) return false;
return killTime >= oneHourAgo;
});
return recentKills.length; // Return 0 if no recent kills, not null
}
// Return null only if we don't have detailed kills data for this system
return null;
}, [detailedKills, systemId]);
useEffect(() => {
// Always prefer the calculated 1-hour count over initial count
// This ensures we properly expire old kills
if (oneHourKillCount !== null) {
setKillsCount(oneHourKillCount);
} else if (detailedKills[systemId] && detailedKills[systemId].length === 0) {
// If we have detailed kills data but it's empty, set to 0
setKillsCount(0);
} else {
// Only fall back to initial count if we have no detailed kills data at all
setKillsCount(initialKillsCount);
}
}, [oneHourKillCount, initialKillsCount, detailedKills, systemId]);
setKillsCount(initialKillsCount);
}, [initialKillsCount]);
const handleEvent = useCallback(
(event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(kill => kill.solar_system_id.toString() === systemId.toString());
if (killForSystem && typeof killForSystem.kills === 'number') {
// Only update if we don't have detailed kills data
if (!detailedKills[systemId] || detailedKills[systemId].length === 0) {
setKillsCount(killForSystem.kills);
}
}
return true;
const handleEvent = useCallback((event: MapEvent): boolean => {
if (event.name === Commands.killsUpdated && Array.isArray(event.payload)) {
const killForSystem = event.payload.find(
kill => kill.solar_system_id.toString() === systemId.toString()
);
if (killForSystem && typeof killForSystem.kills === 'number') {
setKillsCount(killForSystem.kills);
}
return false;
},
[systemId, detailedKills],
);
return true;
}
return false;
}, [systemId]);
useMapEventListener(handleEvent);

View File

@@ -5,11 +5,11 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick';
import { Regions, REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
import { sortWHClasses } from '@/hooks/Mapper/helpers';
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature } from '@/hooks/Mapper/types';
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
import { useSystemName } from './useSystemName';
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
@@ -21,51 +21,11 @@ function getActivityType(count: number): string {
return 'activityDanger';
}
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
isRally: boolean;
classTitle: string | null;
temporaryName?: string | null;
}
const SpaceToClass: Record<string, string> = {
[Spaces.Caldari]: 'Caldaria',
[Spaces.Matar]: 'Mataria',
[Spaces.Amarr]: 'Amarria',
[Spaces.Gallente]: 'Gallente',
[Spaces.Pochven]: 'Pochven',
};
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
@@ -81,7 +41,7 @@ export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
return { localCounterCharacters };
}
export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars => {
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
const { id, data, selected } = props;
const {
id: solar_system_id,
@@ -95,14 +55,10 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
} = data;
const {
storedSettings: { interfaceSettings },
interfaceSettings,
data: { systemSignatures: mapSystemSignatures },
} = useMapRootState();
const systemStaticInfo = useMemo(() => {
return getSystemStaticInfo(solar_system_id)!;
}, [solar_system_id]);
const {
system_class,
security,
@@ -113,8 +69,9 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
region_id,
is_shattered,
solar_system_name,
constellation_name,
} = systemStaticInfo;
} = useMemo(() => {
return getSystemStaticInfo(parseInt(solar_system_id))!;
}, [solar_system_id]);
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
@@ -133,7 +90,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
visibleNodes,
showKSpaceBG,
isThickConnections,
pings,
},
outCommand,
} = useMapState();
@@ -174,7 +130,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const dbClick = useDoubleClick(() => {
outCommand({
type: OutCommand.openSettings,
data: { system_id: solar_system_id },
data: { system_id: solar_system_id.toString() },
});
});
@@ -186,29 +142,16 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
const { systemName, computedTemporaryName, customName } = useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name: solar_system_name || '',
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
systemStaticInfo,
});
const { unsplashedLeft, unsplashedRight } = useUnsplashedSignatures(systemSigs, isShowUnsplashedSignatures);
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
const isRally = useMemo(
() => !!pings.find(x => x.solar_system_id === solar_system_id && x.type === PingType.Rally),
[pings, solar_system_id],
);
const regionName = useMemo(() => {
if (region_id === Regions.Pochven) {
return constellation_name;
}
return region_name;
}, [constellation_name, region_id, region_name]);
const nodeVars: SolarSystemNodeVars = {
id,
selected,
@@ -243,10 +186,47 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
isThickConnections,
classTitle: class_title,
temporaryName: computedTemporaryName,
regionName,
regionName: region_name,
solarSystemName: solar_system_name,
isRally,
};
return nodeVars;
};
}
export interface SolarSystemNodeVars {
id: string;
selected: boolean;
visible: boolean;
isWormhole: boolean;
classTitleColor: string | null;
killsCount: number | null;
killsActivityType: string | null;
hasUserCharacters: boolean;
showHandlers: boolean;
regionClass: string | null;
systemName: string;
customName?: string | null;
labelCustom: string | null;
isShattered: boolean;
tag?: string | null;
status?: number;
labelsInfo: LabelInfo[];
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
sortedStatics: Array<string | number>;
effectName: string | null;
regionName: string | null;
solarSystemId: string;
solarSystemName: string | null;
locked: boolean;
hubs: string[];
name: string | null;
isConnecting: boolean;
hoverNodeId: string | null;
charactersInSystem: Array<CharacterTypeRaw>;
userCharacters: string[];
unsplashedLeft: Array<SystemSignature>;
unsplashedRight: Array<SystemSignature>;
isThickConnections: boolean;
classTitle: string | null;
temporaryName?: string | null;
}

View File

@@ -1,34 +1,30 @@
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
// useSystemName.ts
import { useMemo } from 'react';
interface UseSystemNameParams {
isTempSystemNameEnabled: boolean;
temporary_name?: string | null;
solar_system_name: string;
isShowLinkedSigIdTempName: boolean;
linkedSigPrefix: string | null;
name?: string | null;
systemStaticInfo: SolarSystemStaticInfoRaw;
}
export const useSystemName = ({
export function useSystemName({
isTempSystemNameEnabled,
temporary_name,
solar_system_name,
isShowLinkedSigIdTempName,
linkedSigPrefix,
name,
systemStaticInfo,
}: UseSystemNameParams) => {
const { solar_system_name = '' } = systemStaticInfo;
}: UseSystemNameParams) {
const computedTemporaryName = useMemo(() => {
if (!isTempSystemNameEnabled) {
return '';
}
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
return temporary_name ? `${linkedSigPrefix}:${temporary_name}` : `${linkedSigPrefix}:${solar_system_name}`;
return temporary_name ? `${linkedSigPrefix}${temporary_name}` : `${linkedSigPrefix}${solar_system_name}`;
}
return temporary_name ?? '';
}, [isTempSystemNameEnabled, temporary_name, solar_system_name, isShowLinkedSigIdTempName, linkedSigPrefix]);
@@ -36,7 +32,6 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName) {
return computedTemporaryName;
}
return solar_system_name;
}, [isTempSystemNameEnabled, computedTemporaryName, solar_system_name]);
@@ -44,13 +39,11 @@ export const useSystemName = ({
if (isTempSystemNameEnabled && computedTemporaryName && name) {
return name;
}
if (solar_system_name !== name && name) {
return name;
}
return null;
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
return { systemName, computedTemporaryName, customName };
};
}

View File

@@ -1,48 +1,37 @@
import { Position, internalsSymbol, Node } from 'reactflow';
import { Position, internalsSymbol } from 'reactflow';
type Coords = [number, number];
type CoordsWithPosition = [number, number, Position];
function segmentsIntersect(a1: number, a2: number, b1: number, b2: number): boolean {
const [minA, maxA] = a1 < a2 ? [a1, a2] : [a2, a1];
const [minB, maxB] = b1 < b2 ? [b1, b2] : [b2, b1];
return maxA >= minB && maxB >= minA;
}
function getParams(nodeA: Node, nodeB: Node): CoordsWithPosition {
// returns the position (top,right,bottom or right) passed node compared to
function getParams(nodeA, nodeB) {
const centerA = getNodeCenter(nodeA);
const centerB = getNodeCenter(nodeB);
const horizontalDiff = Math.abs(centerA.x - centerB.x);
const verticalDiff = Math.abs(centerA.y - centerB.y);
let position: Position;
if (
segmentsIntersect(
nodeA.positionAbsolute!.x - 10,
nodeA.positionAbsolute!.x - 10 + nodeA.width! + 20,
nodeB.positionAbsolute!.x,
nodeB.positionAbsolute!.x + nodeB.width!,
)
) {
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
} else {
// when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
if (horizontalDiff > verticalDiff) {
position = centerA.x > centerB.x ? Position.Left : Position.Right;
} else {
// here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
}
const [x, y] = getHandleCoordsByPosition(nodeA, position);
return [x, y, position];
}
function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords {
const handle = node[internalsSymbol]!.handleBounds!.source!.find(h => h.position === handlePosition);
if (!handle) {
throw new Error(`Handle with position ${handlePosition} not found on node ${node.id}`);
}
function getHandleCoordsByPosition(node, handlePosition) {
// all handles are from type source, that's why we use handleBounds.source here
const handle = node[internalsSymbol].handleBounds.source.find(h => h.position === handlePosition);
let offsetX = handle.width / 2;
let offsetY = handle.height / 2;
// this is a tiny detail to make the markerEnd of an edge visible.
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
switch (handlePosition) {
case Position.Left:
offsetX = 0;
@@ -58,20 +47,21 @@ function getHandleCoordsByPosition(node: Node, handlePosition: Position): Coords
break;
}
const x = node.positionAbsolute!.x + handle.x + offsetX;
const y = node.positionAbsolute!.y + handle.y + offsetY;
const x = node.positionAbsolute.x + handle.x + offsetX;
const y = node.positionAbsolute.y + handle.y + offsetY;
return [x, y];
}
function getNodeCenter(node: Node): { x: number; y: number } {
function getNodeCenter(node) {
return {
x: node.positionAbsolute!.x + node.width! / 2,
y: node.positionAbsolute!.y + node.height! / 2,
x: node.positionAbsolute.x + node.width / 2,
y: node.positionAbsolute.y + node.height / 2,
};
}
export function getEdgeParams(source: Node, target: Node) {
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
export function getEdgeParams(source, target) {
const [sx, sy, sourcePos] = getParams(source, target);
const [tx, ty, targetPos] = getParams(target, source);

View File

@@ -1,12 +1,9 @@
import classes from './MarkdownComment.module.scss';
import clsx from 'clsx';
import {
InfoDrawer,
MarkdownTextViewer,
TimeAgo,
TooltipPosition,
WdImgButton,
} from '@/hooks/Mapper/components/ui-kit';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { InfoDrawer, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import remarkBreaks from 'remark-breaks';
import { useGetCacheCharacter } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useCallback, useRef, useState } from 'react';
import { WdTransition } from '@/hooks/Mapper/components/ui-kit/WdTransition/WdTransition.tsx';
@@ -16,6 +13,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top };
const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
export interface MarkdownCommentProps {
text: string;
@@ -81,7 +79,7 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
</div>
}
>
<MarkdownTextViewer>{text}</MarkdownTextViewer>
<Markdown remarkPlugins={REMARK_PLUGINS}>{text}</Markdown>
</InfoDrawer>
<ConfirmPopup

View File

@@ -9,7 +9,6 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export interface CommentsEditorProps {}
// eslint-disable-next-line no-empty-pattern
export const CommentsEditor = ({}: CommentsEditorProps) => {
const [textVal, setTextVal] = useState('');

View File

@@ -1,49 +0,0 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import { RoutesList } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesList';
export const PingRoute = () => {
const {
data: { routes, pings, loadingPublicRoutes },
} = useMapRootState();
const route = useMemo(() => {
const [ping] = pings;
if (!ping) {
return null;
}
return routes?.routes.find(x => ping.solar_system_id === x.destination.toString()) ?? null;
}, [routes, pings]);
const preparedRoute = useMemo(() => {
if (!route) {
return null;
}
return {
...route,
mapped_systems:
route.systems?.map(solar_system_id =>
routes?.systems_static_data.find(
system_static_data => system_static_data.solar_system_id === solar_system_id,
),
) ?? [],
};
}, [route, routes?.systems_static_data]);
if (loadingPublicRoutes) {
return <span className="m-0 text-[12px]">Loading...</span>;
}
if (!preparedRoute || preparedRoute.origin === preparedRoute.destination) {
return null;
}
return (
<div className="m-0 flex gap-2 items-center text-[12px]">
{preparedRoute.has_connection && <div className="text-[12px]">{preparedRoute.systems?.length ?? 2}</div>}
<RoutesList data={preparedRoute} />
</div>
);
};

View File

@@ -1,284 +0,0 @@
import { Button } from 'primereact/button';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Commands, OutCommand, PingType } from '@/hooks/Mapper/types';
import {
CharacterCardById,
SystemView,
TimeAgo,
TooltipPosition,
WdImgButton,
WdImgButtonTooltip,
} from '@/hooks/Mapper/components/ui-kit';
import useRefState from 'react-usestateref';
import { PrimeIcons } from 'primereact/api';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { PingRoute } from '@/hooks/Mapper/components/mapInterface/components/PingsInterface/PingRoute.tsx';
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
[PingsPlacement.leftTop]: 'top-left',
[PingsPlacement.rightBottom]: 'bottom-right',
[PingsPlacement.leftBottom]: 'bottom-left',
};
const PING_PLACEMENT_MAP_OFFSETS = {
[PingsPlacement.rightTop]: { default: '!top-[56px]', withLeftMenu: '!top-[56px] !right-[64px]' },
[PingsPlacement.rightBottom]: { default: '!bottom-[15px]', withLeftMenu: '!bottom-[15px] !right-[64px]' },
[PingsPlacement.leftTop]: { default: '!top-[56px] !left-[64px]', withLeftMenu: '!top-[56px] !left-[64px]' },
[PingsPlacement.leftBottom]: { default: '!left-[64px] !bottom-[15px]', withLeftMenu: '!bottom-[15px]' },
};
const CLOSE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Hide',
position: TooltipPosition.top,
className: '!leading-[0]',
};
const NAVIGATE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Navigate To',
position: TooltipPosition.top,
className: '!leading-[0]',
};
const DELETE_TOOLTIP_PROPS: WdImgButtonTooltip = {
content: 'Remove',
position: TooltipPosition.top,
className: '!leading-[0]',
};
// const TOOLTIP_WAYPOINT_PROPS: WdImgButtonTooltip = {
// content: 'Waypoint',
// position: TooltipPosition.bottom,
// className: '!leading-[0]',
// };
const TITLES = {
[PingType.Alert]: 'Alert',
[PingType.Rally]: 'Rally Point',
};
const ICONS = {
[PingType.Alert]: 'pi-bell',
[PingType.Rally]: 'pi-bell',
};
export interface PingsInterfaceProps {
hasLeftOffset?: boolean;
}
// TODO: right now can be one ping. But in future will be multiple pings then:
// 1. we will use this as container
// 2. we will create PingInstance (which will contains ping Button and Toast
// 3. ADD Context menu
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const toast = useRef<Toast>(null);
const [isShow, setIsShow, isShowRef] = useRefState(false);
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const {
storedSettings: { interfaceSettings },
data: { pings, selectedSystems },
outCommand,
} = useMapRootState();
const selectedSystem = useMemo(() => {
if (selectedSystems.length !== 1) {
return null;
}
return selectedSystems[0];
}, [selectedSystems]);
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const navigateTo = useCallback(() => {
if (!ping) {
return;
}
emitMapEvent({
name: Commands.centerSystem,
data: ping.solar_system_id?.toString(),
});
}, [ping]);
const removePing = useCallback(async () => {
if (!ping) {
return;
}
await outCommand({
type: OutCommand.cancelPing,
data: { type: ping.type, solar_system_id: ping.solar_system_id },
});
}, [outCommand, ping]);
useEffect(() => {
if (!ping) {
return;
}
const tid = setTimeout(() => {
toast.current?.replace({ severity: 'warn', detail: ping.message });
setIsShow(true);
}, 200);
return () => clearTimeout(tid);
}, [ping]);
const handleClickShow = useCallback(() => {
if (!ping) {
return;
}
if (!isShowRef.current) {
toast.current?.show({ severity: 'warn', detail: ping.message });
setIsShow(true);
return;
}
toast.current?.clear();
setIsShow(false);
}, [ping]);
const handleClickHide = useCallback(() => {
toast.current?.clear();
setIsShow(false);
}, []);
const { placement, offsets } = useMemo(() => {
const rawPlacement =
interfaceSettings.pingsPlacement == null ? PingsPlacement.rightTop : interfaceSettings.pingsPlacement;
return {
placement: PING_PLACEMENT_MAP[rawPlacement],
offsets: PING_PLACEMENT_MAP_OFFSETS[rawPlacement],
};
}, [interfaceSettings]);
if (!ping) {
return null;
}
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
return (
<>
<Toast
position={placement as never}
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
ref={toast}
content={({ message }) => (
<section
className={clsx(
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
)}
>
<div className="flex gap-3">
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
<div className="flex flex-col gap-1 w-full">
<div className="flex justify-between">
<div>
<div className="m-0 font-semibold text-base text-white">{TITLES[ping.type]}</div>
<div className="flex gap-1 items-center">
{isShowSelectedSystem && (
<>
<SystemView systemId={selectedSystem} />
<span className="pi pi-angle-double-right text-[10px] relative top-[1px] text-stone-400" />
</>
)}
<SystemView systemId={ping.solar_system_id} />
{isShowSelectedSystem && (
<WdImgButton
className={clsx(PrimeIcons.QUESTION_CIRCLE, 'ml-[2px] relative top-[-2px] !text-[10px]')}
tooltip={{
position: TooltipPosition.top,
content: (
<div className="flex flex-col gap-1">
The settings for the route are taken from the Routes settings and can be configured
through them.
</div>
),
}}
/>
)}
</div>
</div>
<div className="flex flex-col items-end">
<CharacterCardById className="" characterId={ping.character_eve_id} simpleMode />
<TimeAgo timestamp={ping.inserted_at.toString()} className="text-stone-400 text-[11px]" />
</div>
</div>
{selectedSystem != null && <PingRoute />}
<p className="m-0 text-[13px] text-stone-200 min-h-[20px] pr-[16px]">{message.detail}</p>
</div>
<WdImgButton
className={clsx(PrimeIcons.TIMES, 'hover:text-red-400 mt-[3px]')}
tooltip={CLOSE_TOOLTIP_PROPS}
onClick={handleClickHide}
/>
</div>
{/*Button bar*/}
<div className="flex justify-end items-center gap-2 h-0 relative top-[-8px]">
<WdImgButton
className={clsx('pi-compass', 'hover:text-red-400 mt-[3px]')}
tooltip={NAVIGATE_TOOLTIP_PROPS}
onClick={navigateTo}
/>
{/*@ts-ignore*/}
<div ref={cpRemoveBtnRef}>
<WdImgButton
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
tooltip={DELETE_TOOLTIP_PROPS}
onClick={handleShowCP}
/>
</div>
{/* TODO ADD solar system menu*/}
{/*<WdImgButton*/}
{/* className={clsx('pi-map-marker', 'hover:text-red-400 mt-[3px]')}*/}
{/* tooltip={TOOLTIP_WAYPOINT_PROPS}*/}
{/* onClick={handleClickHide}*/}
{/*/>*/}
</div>
</section>
)}
></Toast>
<Button
icon="pi pi-bell"
severity="warning"
aria-label="Notification"
size="small"
className="w-[33px] h-[33px]"
outlined
onClick={handleClickShow}
disabled={isShow}
/>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}
/>
</>
);
};

View File

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

View File

@@ -1,23 +1,23 @@
import { useCallback, useMemo, useRef } from 'react';
import { Dialog } from 'primereact/dialog';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
import {
SOLAR_SYSTEM_CLASS_IDS,
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
} from '@/hooks/Mapper/components/map/constants.ts';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import {
SETTINGS_KEYS,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
import { getWhSize } from '@/hooks/Mapper/helpers/getWhSize';
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
@@ -49,9 +49,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
ref.current = { outCommand };
// Get system info for the target system
const { staticInfo: targetSystemInfo, dynamicInfo: targetSystemDynamicInfo } = useSystemInfo({
systemId: `${data.solar_system_target}`,
});
const { staticInfo: targetSystemInfo } = useSystemInfo({ systemId: `${data.solar_system_target}` });
// Get the system class group for the target system
const targetSystemClassGroup = useMemo(() => {
@@ -146,7 +144,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
}
const whShipSize = getWhSize(wormholes, signature.type);
if (whShipSize !== undefined && whShipSize !== null) {
if (whShipSize) {
await outCommand({
type: OutCommand.updateConnectionShipSizeType,
data: {
@@ -162,12 +160,6 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
[data, setVisible, wormholes],
);
useEffect(() => {
if (!targetSystemDynamicInfo) {
handleHide();
}
}, [targetSystemDynamicInfo]);
return (
<Dialog
header="Select signature to link"

View File

@@ -1,101 +0,0 @@
import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemView } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
const PING_TITLES = {
[PingType.Rally]: 'RALLY',
[PingType.Alert]: 'ALERT',
};
interface SystemPingDialogProps {
systemId: string;
type: PingType;
visible: boolean;
setVisible: (visible: boolean) => void;
}
export const SystemPingDialog = ({ systemId, type, visible, setVisible }: SystemPingDialogProps) => {
const { outCommand } = useMapRootState();
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement>();
const ref = useRef({ message, outCommand, systemId, type });
ref.current = { message, outCommand, systemId, type };
const handleSave = useCallback(() => {
const { message, outCommand, systemId, type } = ref.current;
outCommand({
type: OutCommand.addPing,
data: {
solar_system_id: systemId,
type,
message,
},
});
setVisible(false);
}, [setVisible]);
const onShow = useCallback(() => {
inputRef.current?.focus();
}, []);
return (
<Dialog
header={
<div className="flex gap-1 text-[13px] items-center text-stone-300">
<div>Ping:{` `}</div>
<div
className={clsx({
['text-cyan-400']: type === PingType.Rally,
})}
>
{PING_TITLES[type]}
</div>
<div className="text-[11px]">in</div> <SystemView systemId={systemId} className="relative top-[1px]" />
</div>
}
visible={visible}
draggable={false}
style={{ width: '450px' }}
onShow={onShow}
onHide={() => {
if (!visible) {
return;
}
setVisible(false);
}}
>
<form onSubmit={handleSave}>
<div className="flex flex-col gap-3 px-2">
<div className="flex flex-col gap-1">
<label className="text-[11px]" htmlFor="username">
Message
</label>
<InputTextarea
// @ts-ignore
ref={inputRef}
autoResize
rows={3}
cols={30}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</div>
<div className="flex gap-2 justify-end">
<Button onClick={handleSave} size="small" severity="danger" label="Ping!"></Button>
</div>
</div>
</form>
</Dialog>
);
};

View File

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

View File

@@ -206,7 +206,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
aria-describedby="temporaryName"
autoComplete="off"
value={temporaryName}
maxLength={12}
maxLength={10}
onChange={e => setTemporaryName(e.target.value)}
/>
</IconField>

View File

@@ -2,4 +2,3 @@ export * from './Widget';
export * from './SystemSettingsDialog';
export * from './SystemCustomLabelDialog';
export * from './SystemLinkSignatureDialog';
export * from './PingsInterface';

View File

@@ -1,14 +1,13 @@
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
import {
CommentsWidget,
LocalCharacters,
RoutesWidget,
SystemInfo,
SystemSignatures,
SystemStructures,
WRoutesPublic,
WRoutesUser,
WSystemKills,
} from '@/hooks/Mapper/components/mapInterface/widgets';
import { CommentsWidget } from '@/hooks/Mapper/components/mapInterface/widgets/CommentsWidget';
export const CURRENT_WINDOWS_VERSION = 9;
export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2';
@@ -21,7 +20,6 @@ export enum WidgetsIds {
structures = 'structures',
kills = 'kills',
comments = 'comments',
userRoutes = 'userRoutes',
}
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
@@ -58,14 +56,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
position: { x: 10, y: 530 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesPublic />,
},
{
id: WidgetsIds.userRoutes,
position: { x: 10, y: 10 },
size: { width: 510, height: 200 },
zIndex: 0,
content: () => <WRoutesUser />,
content: () => <RoutesWidget />,
},
{
id: WidgetsIds.structures,
@@ -112,10 +103,6 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
id: WidgetsIds.routes,
label: 'Routes',
},
{
id: WidgetsIds.userRoutes,
label: 'User Routes',
},
{
id: WidgetsIds.structures,
label: 'Structures',

View File

@@ -1,10 +1,6 @@
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
export const sortCharacters = (a: CharacterTypeRaw & WithIsOwnCharacter, b: CharacterTypeRaw & WithIsOwnCharacter) => {
if (a.online === b.online) {
return a.name.localeCompare(b.name);
}
if (a.online !== b.online) {
return a.online && !b.online ? -1 : 1;
}

View File

@@ -1,8 +1,8 @@
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import clsx from 'clsx';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import classes from './LocalCharactersItemTemplate.module.scss';
import clsx from 'clsx';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { CharItemProps } from '@/hooks/Mapper/components/mapInterface/widgets/LocalCharacters/components';
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
export type LocalCharactersItemTemplateProps = { showShipName: boolean } & CharItemProps &
VirtualScrollerTemplateOptions;
@@ -22,7 +22,7 @@ export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalC
)}
style={{ height: `${options.props.itemSize}px` }}
>
<CharacterCard showShipName={showShipName} showTicker showShip {...options} />
<CharacterCard showShipName={showShipName} {...options} />
</div>
);
};

View File

@@ -1,31 +1,79 @@
import React, { createContext, forwardRef, useContext } from 'react';
import {
RoutesImperativeHandle,
RoutesProviderInnerProps,
RoutesWidgetProps,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import React, { createContext, useContext, useEffect } from 'react';
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
type MapProviderProps = {
export type RoutesType = {
path_type: 'shortest' | 'secure' | 'insecure';
include_mass_crit: boolean;
include_eol: boolean;
include_frig: boolean;
include_cruise: boolean;
include_thera: boolean;
avoid_wormholes: boolean;
avoid_pochven: boolean;
avoid_edencom: boolean;
avoid_triglavian: boolean;
avoid: number[];
};
interface MapProviderProps {
children: React.ReactNode;
} & RoutesWidgetProps;
}
const RoutesContext = createContext<RoutesProviderInnerProps>({
export const DEFAULT_SETTINGS: RoutesType = {
path_type: 'shortest',
include_mass_crit: true,
include_eol: true,
include_frig: true,
include_cruise: true,
include_thera: true,
avoid_wormholes: false,
avoid_pochven: false,
avoid_edencom: false,
avoid_triglavian: false,
avoid: [],
};
export interface MapContextProps {
update: ContextStoreDataUpdate<RoutesType>;
data: RoutesType;
}
const RoutesContext = createContext<MapContextProps>({
update: () => {},
// @ts-ignore
data: {},
data: { ...DEFAULT_SETTINGS },
});
// INFO: this component have imperative handler but now it not using.
export const RoutesProvider = forwardRef<RoutesImperativeHandle, MapProviderProps>(
({ children, ...props } /*, ref*/) => {
// useImperativeHandle(ref, () => ({}));
export const RoutesProvider: React.FC<MapProviderProps> = ({ children }) => {
const { update, ref } = useContextStore<RoutesType>(
{ ...DEFAULT_SETTINGS },
{
onAfterAUpdate: values => {
localStorage.setItem(SESSION_KEY.routes, JSON.stringify(values));
},
},
);
return <RoutesContext.Provider value={{ ...props /*, loading, setLoading*/ }}>{children}</RoutesContext.Provider>;
},
);
RoutesProvider.displayName = 'RoutesProvider';
useEffect(() => {
const items = localStorage.getItem(SESSION_KEY.routes);
if (items) {
update(JSON.parse(items));
}
}, [update]);
return (
<RoutesContext.Provider
value={{
update,
data: ref,
}}
>
{children}
</RoutesContext.Provider>
);
};
export const useRouteProvider = () => {
const context = useContext<RoutesProviderInnerProps>(RoutesContext);
const context = useContext<MapContextProps>(RoutesContext);
return context;
};

View File

@@ -2,16 +2,16 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
LayoutEventBlocker,
LoadingWrapper,
SystemView,
SystemViewStandalone,
TooltipPosition,
WdCheckbox,
WdImgButton,
} from '@/hooks/Mapper/components/ui-kit';
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
import { forwardRef, MouseEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getSystemById } from '@/hooks/Mapper/helpers/getSystemById.ts';
import classes from './RoutesWidget.module.scss';
import { useLoadRoutes } from './hooks';
import { RoutesList } from './RoutesList';
import clsx from 'clsx';
import { Route } from '@/hooks/Mapper/types/routes.ts';
@@ -25,10 +25,7 @@ import {
AddSystemDialog,
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import {
RoutesImperativeHandle,
RoutesWidgetProps,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { OutCommand } from '@/hooks/Mapper/types';
const sortByDist = (a: Route, b: Route) => {
const distA = a.has_connection ? a.systems?.length || 0 : Infinity;
@@ -39,31 +36,45 @@ const sortByDist = (a: Route, b: Route) => {
export const RoutesWidgetContent = () => {
const {
data: { selectedSystems, systems, isSubscriptionActive },
data: { selectedSystems, hubs = [], systems, routes },
outCommand,
} = useMapRootState();
const { hubs = [], routesList, isRestricted, loading } = useRouteProvider();
const [systemId] = selectedSystems;
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] });
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers();
const { loading } = useLoadRoutes();
const { systems: systemStatics, loadSystems, lastUpdateKey } = useLoadSystemStatic({ systems: hubs ?? [] });
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
outCommand,
hubs,
});
const preparedHubs = useMemo(() => {
return hubs.map(x => {
const sys = getSystemById(systems, x.toString());
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) };
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hubs, systems, systemStatics, lastUpdateKey]);
const preparedRoutes: Route[] = useMemo(() => {
return (
routesList?.routes
routes?.routes
.sort(sortByDist)
// .filter(x => x.destination.toString() !== systemId)
.filter(x => x.destination.toString() !== systemId)
.map(route => ({
...route,
mapped_systems:
route.systems?.map(solar_system_id =>
routesList?.systems_static_data.find(
routes?.systems_static_data.find(
system_static_data => system_static_data.solar_system_id === solar_system_id,
),
) ?? [],
})) ?? []
);
}, [routesList?.routes, routesList?.systems_static_data, systemId]);
}, [routes?.routes, routes?.systems_static_data, systemId]);
const refData = useRef({ open, loadSystems, preparedRoutes });
refData.current = { open, loadSystems, preparedRoutes };
@@ -86,13 +97,9 @@ export const RoutesWidgetContent = () => {
[handleClick],
);
if (isRestricted && !isSubscriptionActive) {
if (loading) {
return (
<div className="w-full h-full flex items-center justify-center">
<span className="select-none text-center text-stone-400/80 text-sm">
User Routes available with &#39;Active&#39; map subscription only (contact map administrators)
</span>
</div>
<div className="w-full h-full flex justify-center items-center select-none text-center">Loading routes...</div>
);
}
@@ -110,10 +117,12 @@ export const RoutesWidgetContent = () => {
return (
<>
<LoadingWrapper loading={loading}>
{systemId !== undefined && routes && (
<div className={clsx(classes.RoutesGrid, 'px-2 py-2')}>
{preparedRoutes.map(route => {
// TODO do not delete this console log
const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!;
// TODO do not delte this console log
// eslint-disable-next-line no-console
// console.log('JOipP', `Check sys [${route.destination}]:`, sys);
@@ -123,19 +132,15 @@ export const RoutesWidgetContent = () => {
<WdImgButton
className={clsx(PrimeIcons.BARS, classes.RemoveBtn)}
onClick={e => handleClick(e, route.destination.toString())}
tooltip={{
content: 'Click here to open system menu',
position: TooltipPosition.top,
offset: 10,
}}
tooltip={{ content: 'Click here to open system menu', position: TooltipPosition.top, offset: 10 }}
/>
<SystemView
systemId={route.destination.toString()}
<SystemViewStandalone
key={route.destination}
className={clsx('select-none text-center cursor-context-menu')}
hideRegion
compact
showCustomName
{...sys}
/>
</div>
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
@@ -146,7 +151,7 @@ export const RoutesWidgetContent = () => {
);
})}
</div>
</LoadingWrapper>
)}
<ContextMenuSystemInfo
hubs={hubs}
@@ -160,13 +165,15 @@ export const RoutesWidgetContent = () => {
);
};
type RoutesWidgetCompProps = {
title: ReactNode | string;
};
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
export const RoutesWidgetComp = () => {
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
const { data, update, addHubCommand } = useRouteProvider();
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(() => {
@@ -183,15 +190,24 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
async item => addHubCommand(item.value.toString()),
[addHubCommand],
async item => {
if (preparedHubs.includes(item.value)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: item.value },
});
},
[hubs, outCommand],
);
return (
<Widget
label={
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
<span className="select-none">{title}</span>
<span className="select-none">Routes</span>
<LayoutEventBlocker className="flex items-center gap-2">
<WdImgButton
className={PrimeIcons.PLUS_CIRCLE}
@@ -215,7 +231,6 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
className={PrimeIcons.SLIDERS_H}
onClick={() => setRouteSettingsVisible(true)}
tooltip={{
position: TooltipPosition.top,
content: 'Click here to open Routes settings',
}}
/>
@@ -236,13 +251,10 @@ export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
);
};
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
({ title, ...props }, ref) => {
return (
<RoutesProvider {...props} ref={ref}>
<RoutesWidgetComp title={title} />
</RoutesProvider>
);
},
);
RoutesWidget.displayName = 'RoutesWidget';
export const RoutesWidget = () => {
return (
<RoutesProvider>
<RoutesWidgetComp />
</RoutesProvider>
);
};

View File

@@ -1,8 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
import {
RoutesType,
useRouteProvider,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
@@ -14,25 +16,13 @@ function usePrevious<T>(value: T): T | undefined {
return ref.current;
}
type UseLoadRoutesProps = {
loadRoutesCommand: LoadRoutesCommand;
hubs: string[];
routesList: RoutesList | undefined;
data: RoutesType;
deps?: unknown[];
};
export const useLoadRoutes = ({
data: routesSettings,
loadRoutesCommand,
hubs,
routesList,
deps = [],
}: UseLoadRoutesProps) => {
export const useLoadRoutes = () => {
const [loading, setLoading] = useState(false);
const { data: routesSettings } = useRouteProvider();
const {
data: { selectedSystems, systems, connections },
outCommand,
data: { selectedSystems, hubs, systems, connections },
} = useMapRootState();
const prevSys = usePrevious(systems);
@@ -41,16 +31,17 @@ export const useLoadRoutes = ({
const loadRoutes = useCallback(
(systemId: string, routesSettings: RoutesType) => {
loadRoutesCommand(systemId, routesSettings);
setLoading(true);
outCommand({
type: OutCommand.getRoutes,
data: {
system_id: systemId,
routes_settings: routesSettings,
},
});
},
[loadRoutesCommand],
[outCommand],
);
useEffect(() => {
setLoading(false);
}, [routesList]);
useEffect(() => {
if (selectedSystems.length !== 1) {
return;
@@ -70,8 +61,7 @@ export const useLoadRoutes = ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
.map(x => routesSettings[x]),
...deps,
]);
return { loading, loadRoutes, setLoading };
return { loading, loadRoutes };
};

View File

@@ -1,24 +0,0 @@
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
export type LoadRoutesCommand = (systemId: string, routesSettings: RoutesType) => Promise<void>;
export type AddHubCommand = (systemId: string) => Promise<void>;
export type ToggleHubCommand = (systemId: string) => Promise<void>;
export type RoutesWidgetProps = {
data: RoutesType;
update: (d: RoutesType) => void;
hubs: string[];
routesList: RoutesList | undefined;
loading: boolean;
addHubCommand: AddHubCommand;
toggleHubCommand: ToggleHubCommand;
isRestricted?: boolean;
};
export type RoutesProviderInnerProps = RoutesWidgetProps;
export type RoutesImperativeHandle = {
stopLoading: () => void;
};

View File

@@ -1,9 +1,9 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { LayoutEventBlocker, SystemView, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react';
import { useState, useCallback } from 'react';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
@@ -42,7 +42,7 @@ export const SystemInfo = () => {
<WdImgButton
className="pi pi-pen-to-square"
onClick={() => setVisible(true)}
tooltip={{ position: TooltipPosition.top, content: 'Edit system name and description' }}
tooltip={{ content: 'Edit system name and description' }}
/>
</LayoutEventBlocker>
</div>

View File

@@ -10,7 +10,7 @@ export interface SignatureViewProps {
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
const isWormhole = signature?.group === SignatureGroup.Wormhole;
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature;
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : (signature?.group ?? SignatureGroup.CosmicSignature);
const characterName = signature.character_name || 'Unknown character';
return (

View File

@@ -19,7 +19,7 @@ export type HeaderProps = {
lazyDeleteValue: boolean;
onLazyDeleteChange: (checked: boolean) => void;
pendingCount: number;
undoCountdown?: number;
pendingTimeRemaining?: number; // Time remaining in ms
onUndoClick: () => void;
onSettingsClick: () => void;
};
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
lazyDeleteValue,
onLazyDeleteChange,
pendingCount,
undoCountdown,
pendingTimeRemaining,
onUndoClick,
onSettingsClick,
}: HeaderProps) => {
@@ -43,6 +43,13 @@ export const SystemSignaturesHeader = ({
const containerRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
// Format time remaining as seconds
const formatTimeRemaining = () => {
if (!pendingTimeRemaining) return '';
const seconds = Math.ceil(pendingTimeRemaining / 1000);
return ` (${seconds}s remaining)`;
};
return (
<div ref={containerRef} className="w-full">
<div className="flex justify-between items-center text-xs w-full h-full">
@@ -71,9 +78,7 @@ export const SystemSignaturesHeader = ({
<WdImgButton
className={PrimeIcons.UNDO}
style={{ color: 'red' }}
tooltip={{
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? `${undoCountdown}s left` : ''}`,
}}
tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
onClick={onUndoClick}
/>
)}

View File

@@ -1,179 +1,99 @@
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import useLocalStorageState from 'use-local-storage-state';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import {
SETTINGS_KEYS,
SETTINGS_VALUES,
SIGNATURE_DELETION_TIMEOUTS,
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SIGNATURES_DELETION_TIMING,
SignatureSettingsType,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
const intervalRef = useRef<number | null>(null);
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
const newIds = signatures.map(sig => sig.eve_id);
setPendingIds(prev => {
const next = new Set(prev);
newIds.forEach(id => next.add(id));
return next;
});
setDeletedSignatures(prev => [...prev, ...signatures]);
}, []);
// Clear deleted signatures when system changes
useEffect(() => {
if (systemId) {
setDeletedSignatures([]);
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
}, [systemId]);
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.size === 0) {
setCountdown(0);
setDeletedSignatures([]);
return;
}
// determine timeout from settings
const timeoutMs = getDeletionTimeoutMs(settings);
setCountdown(Math.ceil(timeoutMs / 1000));
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
setDeletedSignatures([]);
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [pendingIds, settings]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || pendingIds.size === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setDeletedSignatures([]);
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
deletedSignatures,
addDeleted,
handleUndo,
};
}
import { calculateTimeRemaining } from './helpers';
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0);
const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
const undoPendingFnRef = useRef<() => void>(() => {});
const {
data: { selectedSystems },
outCommand,
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
SIGNATURE_SETTING_STORE_KEY,
{
defaultValue: SETTINGS_VALUES,
},
);
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
systemId,
currentSettings,
outCommand,
);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
defaultValue: SETTINGS_VALUES,
});
const handleCountChange = useCallback((count: number) => {
const handleSigCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
const handleSettingsChange = useCallback((newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
}, []);
const handleLazyDeleteChange = useCallback((value: boolean) => {
setCurrentSettings(prev => ({ ...prev, [SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value }));
}, []);
useHotkey(true, ['z'], event => {
if (pendingSigs.length > 0) {
event.preventDefault();
event.stopPropagation();
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}
});
const handleUndoClick = useCallback(() => {
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}, []);
const handleSettingsButtonClick = useCallback(() => {
setVisible(true);
}, []);
const handlePendingChange = useCallback(
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
setPendingSigs(() => {
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
});
undoPendingFnRef.current = newUndo;
},
[setCurrentSettings],
[],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
setCurrentSettings(prev => ({
...prev,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
}));
},
[setCurrentSettings],
);
// Calculate the minimum time remaining for any pending signature
useEffect(() => {
if (pendingSigs.length === 0) {
setPendingTimeRemaining(undefined);
return;
}
const openSettings = useCallback(() => setVisible(true), []);
const calculate = () => {
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
};
calculate();
const interval = setInterval(calculate, 1000);
return () => clearInterval(interval);
}, [pendingSigs]);
return (
<Widget
@@ -181,16 +101,16 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingIds.size}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
onUndoClick={handleUndo}
onSettingsClick={openSettings}
pendingCount={pendingSigs.length}
pendingTimeRemaining={pendingTimeRemaining}
onLazyDeleteChange={handleLazyDeleteChange}
onUndoClick={handleUndoClick}
onSettingsClick={handleSettingsButtonClick}
/>
}
windowId={SIGNATURE_WINDOW_ID}
>
{!isSystemSelected ? (
{isNotSelectedSystem ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
System is not selected
</div>
@@ -198,18 +118,22 @@ export const SystemSignatures = () => {
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
deletedSignatures={deletedSignatures}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}
deletionTiming={
SIGNATURE_DELETION_TIMEOUTS[
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
SIGNATURES_DELETION_TIMING.DEFAULT
] as number
}
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={handlePendingChange}
/>
)}
{visible && (
<SystemSignatureSettingsDialog
settings={currentSettings}
onCancel={() => setVisible(false)}
onSave={handleSettingsSave}
onSave={handleSettingsChange}
/>
)}
</Widget>

View File

@@ -57,9 +57,12 @@ interface SystemSignaturesContentProps {
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
deletionTiming?: number;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
deletedSignatures?: ExtendedSystemSignature[];
}
export const SystemSignaturesContent = ({
@@ -70,9 +73,9 @@ export const SystemSignaturesContent = ({
onSelect,
onLazyDeleteChange,
onCountChange,
onPendingChange,
deletionTiming,
filterSignature,
onSignatureDeleted,
deletedSignatures = [],
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -92,21 +95,15 @@ export const SystemSignaturesContent = ({
{ defaultValue: SORT_DEFAULT_VALUES },
);
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings,
onCountChange,
onLazyDeleteChange,
onSignatureDeleted,
});
const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } =
useSystemSignaturesData({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
});
useEffect(() => {
if (selectable) return;
@@ -128,8 +125,6 @@ export const SystemSignaturesContent = ({
event.preventDefault();
event.stopPropagation();
// Delete key should always immediately delete, never show pending deletions
handleDeleteSelected();
});
@@ -158,16 +153,9 @@ export const SystemSignaturesContent = ({
const handleSelectSignatures = useCallback(
(e: { value: SystemSignature[] }) => {
// Filter out deleted signatures from selection
const selectableSignatures = e.value.filter(
sig => !deletedSignatures.some(deleted => deleted.eve_id === sig.eve_id),
);
selectable
? onSelect?.(selectableSignatures[0])
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
},
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
[selectable],
);
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
@@ -181,11 +169,7 @@ export const SystemSignaturesContent = ({
);
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
// Get the set of deleted signature IDs for quick lookup
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
// Common filter function
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
return signatures.filter(sig => {
if (filterSignature && !filterSignature(sig)) {
return false;
}
@@ -204,37 +188,15 @@ export const SystemSignaturesContent = ({
x => GROUPS_LIST.includes(x as SignatureGroup) && settings[x as SETTINGS_KEYS],
);
const mappedGroup = getGroupIdByRawGroup(sig.group);
if (!mappedGroup) {
return true; // If we can't determine the group, still show it
}
return enabledGroups.includes(mappedGroup);
return enabledGroups.includes(getGroupIdByRawGroup(sig.group));
}
return true;
}
return settings[sig.kind] as boolean;
};
// Filter active signatures, excluding any that are in the deleted list
const activeSignatures = signatures.filter(sig => {
// Skip if this signature is in the deleted list
if (deletedIds.has(sig.eve_id)) {
return false;
}
return shouldShowSignature(sig);
return settings[sig.kind];
});
// Add deleted signatures with pending deletion flag, applying the same filters
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
...sig,
pendingDeletion: true,
}));
return [...activeSignatures, ...deletedWithPendingFlag];
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
setHoveredSignature(e.data as SystemSignature);
@@ -274,122 +236,113 @@ export const SystemSignaturesContent = ({
No signatures
</div>
) : (
<>
{hasUnsupportedLanguage && (
<div className="w-full flex justify-center items-center text-amber-500 text-xs p-1 bg-amber-950/20 border-b border-amber-800/30">
<i className={PrimeIcons.EXCLAMATION_TRIANGLE + ' mr-1'} />
Non-English signatures detected. Some signatures may not display correctly. Double-click to edit signature
details.
</div>
)}
<DataTable
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
className="w-full select-none"
resizableColumns={false}
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={handleSortSettings}
onRowMouseEnter={onRowMouseEnter}
onRowMouseLeave={onRowMouseLeave}
// @ts-ignore
rowClassName={getRowClassName}
>
<DataTable
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
className="w-full select-none"
resizableColumns={false}
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={handleSortSettings}
onRowMouseEnter={onRowMouseEnter}
onRowMouseLeave={onRowMouseLeave}
// @ts-ignore
rowClassName={getRowClassName}
>
<Column
field="icon"
header=""
body={renderColIcon}
bodyClassName="p-0 px-1"
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
/>
<Column
field="eve_id"
header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
<Column
field="info"
header="Info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column
field="icon"
header=""
body={renderColIcon}
bodyClassName="p-0 px-1"
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
/>
<Column
field="eve_id"
header="Id"
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
body={renderDescription}
sortable
/>
)}
<Column
field="inserted_at"
header="Added"
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
{showUpdatedColumn && (
<Column
field="info"
header="Info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={isCompact}
body={renderDescription}
sortable
/>
)}
<Column
field="inserted_at"
header="Added"
field="updated_at"
header="Updated"
dataType="date"
body={renderAddedTimeLeft}
body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
{showUpdatedColumn && (
<Column
field="updated_at"
header="Updated"
dataType="date"
body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
)}
{showCharacterColumn && (
<Column
field="character_name"
header="Character"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
sortable
></Column>
)}
{showCharacterColumn && (
<Column
field="character_name"
header="Character"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
sortable
></Column>
)}
{!selectable && (
<Column
header=""
body={() => (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper>
</div>
)}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
bodyClassName="p-0 pl-1 pr-2"
/>
)}
</DataTable>
</>
{!selectable && (
<Column
header=""
body={() => (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper>
</div>
)}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
bodyClassName="p-0 pl-1 pr-2"
/>
)}
</DataTable>
)}
<WdTooltip

View File

@@ -1,14 +1,10 @@
import {
GroupType,
SignatureGroup,
SignatureGroupDE,
SignatureGroupENG,
SignatureGroupFR,
SignatureGroupRU,
SignatureKind,
SignatureKindDE,
SignatureKindENG,
SignatureKindFR,
SignatureKindRU,
} from '@/hooks/Mapper/types';
@@ -44,58 +40,46 @@ export const GROUPS: Record<SignatureGroup, GroupType> = {
[SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 },
};
export const LANGUAGE_GROUP_MAPPINGS = {
EN: {
[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,
},
RU: {
[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,
},
FR: {
[SignatureGroupFR.GasSite]: SignatureGroup.GasSite,
[SignatureGroupFR.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupFR.DataSite]: SignatureGroup.DataSite,
[SignatureGroupFR.OreSite]: SignatureGroup.OreSite,
[SignatureGroupFR.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupFR.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupFR.CosmicSignature]: SignatureGroup.CosmicSignature,
},
DE: {
[SignatureGroupDE.GasSite]: SignatureGroup.GasSite,
[SignatureGroupDE.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupDE.DataSite]: SignatureGroup.DataSite,
[SignatureGroupDE.OreSite]: SignatureGroup.OreSite,
[SignatureGroupDE.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupDE.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupDE.CosmicSignature]: SignatureGroup.CosmicSignature,
},
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,
};
// Flatten the structure for backward compatibility
export const MAPPING_GROUP_TO_ENG: Record<string, SignatureGroup> = (() => {
const flattened: Record<string, SignatureGroup> = {};
for (const [, mappings] of Object.entries(LANGUAGE_GROUP_MAPPINGS)) {
Object.assign(flattened, mappings);
}
return flattened;
})();
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,
export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined => {
return MAPPING_GROUP_TO_ENG[val] || undefined;
// 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];
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
@@ -139,7 +123,7 @@ export type Setting = {
name: string;
type: SettingsTypes;
isSeparator?: boolean;
options?: { label: string; value: number | string | boolean }[];
options?: { label: string; value: any }[];
};
export enum SIGNATURES_DELETION_TIMING {
@@ -148,8 +132,7 @@ export enum SIGNATURES_DELETION_TIMING {
EXTENDED,
}
// Now use a stricter type: every timing key maps to a number
export type SignatureDeletionTimingType = Record<SIGNATURES_DELETION_TIMING, number>;
export type SignatureDeletionTimingType = { [key in SIGNATURES_DELETION_TIMING]?: unknown };
export const SIGNATURE_SETTINGS = {
filterFlags: [
@@ -220,73 +203,8 @@ export const SETTINGS_VALUES: SignatureSettingsType = {
[SETTINGS_KEYS.COMBAT_SITE]: true,
};
// Now this map is strongly typed as “number” for each timing enum
export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.DEFAULT]: 10_000,
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
};
/**
* Helper function to extract the deletion timeout in milliseconds from settings
*/
export function getDeletionTimeoutMs(settings: SignatureSettingsType): number {
const raw = settings[SETTINGS_KEYS.DELETION_TIMING];
const timing =
raw && typeof raw === 'object' && 'value' in raw
? (raw as { value: SIGNATURES_DELETION_TIMING }).value
: (raw as SIGNATURES_DELETION_TIMING | undefined);
const validTiming = typeof timing === 'number' ? timing : SIGNATURES_DELETION_TIMING.DEFAULT;
return SIGNATURE_DELETION_TIMEOUTS[validTiming];
}
// Replace the flat structure with a nested structure by language
export const LANGUAGE_TYPE_MAPPINGS = {
EN: {
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindENG.Structure]: SignatureKind.Structure,
[SignatureKindENG.Ship]: SignatureKind.Ship,
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
[SignatureKindENG.Drone]: SignatureKind.Drone,
[SignatureKindENG.Starbase]: SignatureKind.Starbase,
},
RU: {
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindRU.Structure]: SignatureKind.Structure,
[SignatureKindRU.Ship]: SignatureKind.Ship,
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
[SignatureKindRU.Drone]: SignatureKind.Drone,
[SignatureKindRU.Starbase]: SignatureKind.Starbase,
},
FR: {
[SignatureKindFR.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindFR.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindFR.Structure]: SignatureKind.Structure,
[SignatureKindFR.Ship]: SignatureKind.Ship,
[SignatureKindFR.Deployable]: SignatureKind.Deployable,
[SignatureKindFR.Drone]: SignatureKind.Drone,
[SignatureKindFR.Starbase]: SignatureKind.Starbase,
},
DE: {
[SignatureKindDE.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindDE.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindDE.Structure]: SignatureKind.Structure,
[SignatureKindDE.Ship]: SignatureKind.Ship,
[SignatureKindDE.Deployable]: SignatureKind.Deployable,
[SignatureKindDE.Drone]: SignatureKind.Drone,
[SignatureKindDE.Starbase]: SignatureKind.Starbase,
},
};
// Flatten the structure for backward compatibility
export const MAPPING_TYPE_TO_ENG: Record<string, SignatureKind> = (() => {
const flattened: Record<string, SignatureKind> = {};
for (const [, mappings] of Object.entries(LANGUAGE_TYPE_MAPPINGS)) {
Object.assign(flattened, mappings);
}
return flattened;
})();

View File

@@ -1,5 +1,5 @@
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SystemSignature } from '@/hooks/Mapper/types';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { getState } from './getState';
/**
@@ -22,7 +22,6 @@ export const getActualSigs = (
oldSignatures.forEach(oldSig => {
const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id);
if (newSig) {
const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
const mergedSig = { ...oldSig };

View File

@@ -1,15 +1,13 @@
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
import { SystemSignature } from '@/hooks/Mapper/types';
export const getState = (_: string[], newSig: SystemSignature) => {
let state = -1;
if (!newSig.group || newSig.group === SignatureGroup.CosmicSignature) {
if (!newSig.group) {
state = 0;
} else if (!newSig.name || newSig.name === '' || newSig.name === UNKNOWN_SIGNATURE_NAME) {
} else if (!newSig.name || newSig.name === '') {
state = 1;
} else if (newSig.name !== '') {
state = 2;
}
return state;
};

View File

@@ -1,18 +1,23 @@
import { useCallback, useRef } from 'react';
import { useCallback, useRef, useEffect } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload } from '../helpers';
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
}: UsePendingDeletionParams) {
const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
@@ -20,15 +25,63 @@ export function usePendingDeletions({
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
// If deletion timing is 0, immediately delete without pending state
if (finalDuration === 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
return;
}
const now = Date.now();
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingUntil: now + finalDuration,
}));
pendingDeletionMapRef.current = {
...pendingDeletionMapRef.current,
...processedRemoved.reduce((acc: any, sig) => {
acc[sig.eve_id] = sig;
return acc;
}, {}),
};
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
setSignatures(prev =>
prev.map(sig => {
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
}
return sig;
}),
);
scheduleLazyTimers(
processedRemoved,
pendingDeletionMapRef,
async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, [], [], [sig]),
});
delete pendingDeletionMapRef.current[sig.eve_id];
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
},
finalDuration,
);
},
[systemId, outCommand],
[systemId, outCommand, finalDuration],
);
const clearPendingDeletions = useCallback(() => {
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
clearTimeout(finalTimeoutId);
});
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);

View File

@@ -1,19 +1,16 @@
import { useMapEventListener } from '@/hooks/Mapper/events';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { useCallback, useEffect, useState } from 'react';
import useRefState from 'react-usestateref';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { parseSignatures } from '@/hooks/Mapper/helpers';
import {
SETTINGS_KEYS,
getDeletionTimeoutMs,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getActualSigs } from '../helpers';
import { UseSystemSignaturesDataProps } from './types';
import { usePendingDeletions } from './usePendingDeletions';
import { useSignatureFetching } from './useSignatureFetching';
import { usePendingDeletions } from './usePendingDeletions';
import { UseSystemSignaturesDataProps } from './types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { SETTINGS_KEYS } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
export const useSystemSignaturesData = ({
systemId,
@@ -21,18 +18,16 @@ export const useSystemSignaturesData = ({
onCountChange,
onPendingChange,
onLazyDeleteChange,
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
}) => {
deletionTiming,
}: UseSystemSignaturesDataProps) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
});
@@ -47,47 +42,19 @@ export const useSystemSignaturesData = ({
async (clipboardString: string) => {
const lazyDeleteValue = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
// Parse the incoming signatures
const incomingSignatures = parseSignatures(
clipboardString,
Object.keys(settings).filter(skey => skey in SignatureKind),
) as ExtendedSystemSignature[];
if (incomingSignatures.length === 0) {
return;
}
// Check if any signatures might be using unsupported languages
// This is a basic heuristic: if we have signatures where the original group wasn't mapped
const clipboardRows = clipboardString.split('\n').filter(row => row.trim() !== '');
const detectedSignatureCount = clipboardRows.filter(row => row.match(/^[A-Z]{3}-\d{3}/)).length;
// If we detected valid IDs but got fewer parsed signatures, we might have language issues
if (detectedSignatureCount > 0 && incomingSignatures.length < detectedSignatureCount) {
setHasUnsupportedLanguage(true);
} else {
setHasUnsupportedLanguage(false);
}
const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true);
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
// Only show pending deletions if:
// 1. Lazy deletion is enabled AND
// 2. Deletion timing is not immediate (> 0)
if (onSignatureDeleted && lazyDeleteValue) {
const timeoutMs = getDeletionTimeoutMs(settings);
if (timeoutMs > 0) {
onSignatureDeleted(removed);
}
}
}
if (updated.length !== 0 || added.length !== 0) {
@@ -107,23 +74,17 @@ export const useSystemSignaturesData = ({
onLazyDeleteChange?.(false);
}
},
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
);
const handleDeleteSelected = useCallback(async () => {
if (!selectedSignatures.length) return;
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
// IMPORTANT: Send deletion to server BEFORE updating local state
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
await handleUpdateSignatures(finalList, false, true);
// Update local state after server call
setSignatures(finalList);
setSelectedSignatures([]);
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
}, [selectedSignatures, signatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
@@ -154,12 +115,11 @@ export const useSystemSignaturesData = ({
}, [signatures]);
return {
signatures: signatures.filter(sig => !sig.deleted),
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
};
};

View File

@@ -1,69 +0,0 @@
import { Commands, OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
AddHubCommand,
RoutesImperativeHandle,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { useCallback, useRef } from 'react';
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
import { useMapEventListener } from '@/hooks/Mapper/events';
export const WRoutesPublic = () => {
const {
outCommand,
storedSettings: { settingsRoutes, settingsRoutesUpdate },
data: { hubs, routes, loadingPublicRoutes },
} = useMapRootState();
const ref = useRef<RoutesImperativeHandle>(null);
const addHubCommand: AddHubCommand = useCallback(
async systemId => {
if (hubs.includes(systemId)) {
return;
}
await outCommand({
type: OutCommand.addHub,
data: { system_id: systemId },
});
},
[hubs, outCommand],
);
const toggleHubCommand: AddHubCommand = useCallback(
async (systemId: string | undefined) => {
if (!systemId) {
return;
}
outCommand({
type: !hubs.includes(systemId) ? OutCommand.addHub : OutCommand.deleteHub,
data: {
system_id: systemId,
},
});
},
[hubs, outCommand],
);
useMapEventListener(event => {
if (event.name === Commands.routes) {
ref.current?.stopLoading();
}
});
return (
<RoutesWidget
ref={ref}
title="Routes"
data={settingsRoutes}
loading={loadingPublicRoutes}
update={settingsRoutesUpdate}
hubs={hubs}
routesList={routes}
addHubCommand={addHubCommand}
toggleHubCommand={toggleHubCommand}
/>
);
};

View File

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

View File

@@ -1,94 +0,0 @@
import { Commands, OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
AddHubCommand,
LoadRoutesCommand,
RoutesImperativeHandle,
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
import { useCallback, useRef } from 'react';
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { useLoadRoutes } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
export const WRoutesUser = () => {
const {
outCommand,
storedSettings: { settingsRoutes, settingsRoutesUpdate },
data: { userHubs, userRoutes },
} = useMapRootState();
const ref = useRef<RoutesImperativeHandle>(null);
const loadRoutesCommand: LoadRoutesCommand = useCallback(
async (systemId, routesSettings) => {
outCommand({
type: OutCommand.getUserRoutes,
data: {
system_id: systemId,
routes_settings: routesSettings,
},
});
},
[outCommand],
);
const addHubCommand: AddHubCommand = useCallback(
async systemId => {
if (userHubs.includes(systemId)) {
return;
}
await outCommand({
type: OutCommand.addUserHub,
data: { system_id: systemId },
});
},
[userHubs, outCommand],
);
const toggleHubCommand: AddHubCommand = useCallback(
async (systemId: string | undefined) => {
if (!systemId) {
return;
}
outCommand({
type: !userHubs.includes(systemId) ? OutCommand.addUserHub : OutCommand.deleteUserHub,
data: {
system_id: systemId,
},
});
},
[userHubs, outCommand],
);
// INFO: User routes loading only if open widget with user routes
const { loading, setLoading } = useLoadRoutes({
data: settingsRoutes,
hubs: userHubs,
loadRoutesCommand,
routesList: userRoutes,
});
useMapEventListener(event => {
if (event.name === Commands.userRoutes) {
setLoading(false);
}
return true;
});
return (
<RoutesWidget
ref={ref}
title="User Routes"
data={settingsRoutes}
update={settingsRoutesUpdate}
hubs={userHubs}
routesList={userRoutes}
loading={loading}
addHubCommand={addHubCommand}
toggleHubCommand={toggleHubCommand}
isRestricted
/>
);
};

View File

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

View File

@@ -17,91 +17,67 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
export type CompactKillRowProps = {
killDetails?: DetailedKill | null;
killDetails: DetailedKill;
systemName: string;
onlyOneSystem: boolean;
} & WithClassName;
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
const {
killmail_id,
killmail_id = 0,
// Victim data
victim_char_name,
victim_alliance_ticker,
victim_corp_ticker,
victim_ship_name,
victim_corp_name,
victim_alliance_name,
victim_char_id,
victim_corp_id,
victim_alliance_id,
victim_ship_type_id,
victim_char_name = 'Unknown Pilot',
victim_alliance_ticker = '',
victim_corp_ticker = '',
victim_ship_name = 'Unknown Ship',
victim_corp_name = '',
victim_alliance_name = '',
victim_char_id = 0,
victim_corp_id = 0,
victim_alliance_id = 0,
victim_ship_type_id = 0,
// Attacker data
final_blow_char_id,
final_blow_char_name,
final_blow_alliance_ticker,
final_blow_alliance_name,
final_blow_alliance_id,
final_blow_corp_ticker,
final_blow_corp_id,
final_blow_corp_name,
final_blow_ship_type_id,
kill_time,
total_value,
final_blow_char_id = 0,
final_blow_char_name = '',
final_blow_alliance_ticker = '',
final_blow_alliance_name = '',
final_blow_alliance_id = 0,
final_blow_corp_ticker = '',
final_blow_corp_id = 0,
final_blow_corp_name = '',
final_blow_ship_type_id = 0,
kill_time = '',
total_value = 0,
} = killDetails || {};
// Apply fallback values using nullish coalescing to handle both null and undefined
const safeKillmailId = killmail_id ?? 0;
const safeVictimCharName = victim_char_name ?? 'Unknown Pilot';
const safeVictimAllianceTicker = victim_alliance_ticker ?? '';
const safeVictimCorpTicker = victim_corp_ticker ?? '';
const safeVictimShipName = victim_ship_name ?? 'Unknown Ship';
const safeVictimCorpName = victim_corp_name ?? '';
const safeVictimAllianceName = victim_alliance_name ?? '';
const safeVictimCharId = victim_char_id ?? 0;
const safeVictimCorpId = victim_corp_id ?? 0;
const safeVictimAllianceId = victim_alliance_id ?? 0;
const safeVictimShipTypeId = victim_ship_type_id ?? 0;
const safeFinalBlowCharId = final_blow_char_id ?? 0;
const safeFinalBlowCharName = final_blow_char_name ?? '';
const safeFinalBlowAllianceTicker = final_blow_alliance_ticker ?? '';
const safeFinalBlowAllianceName = final_blow_alliance_name ?? '';
const safeFinalBlowAllianceId = final_blow_alliance_id ?? 0;
const safeFinalBlowCorpTicker = final_blow_corp_ticker ?? '';
const safeFinalBlowCorpId = final_blow_corp_id ?? 0;
const safeFinalBlowCorpName = final_blow_corp_name ?? '';
const safeFinalBlowShipTypeId = final_blow_ship_type_id ?? 0;
const safeKillTime = kill_time ?? '';
const safeTotalValue = total_value ?? 0;
const attackerIsNpc = safeFinalBlowCharId === 0;
const attackerIsNpc = final_blow_char_id === 0;
// Define victim affiliation ticker.
const victimAffiliationTicker = safeVictimAllianceTicker || safeVictimCorpTicker || 'No Ticker';
const victimAffiliationTicker = victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
const killValueFormatted = safeTotalValue != null && safeTotalValue > 0 ? `${formatISK(safeTotalValue)} ISK` : null;
const killTimeAgo = safeKillTime ? formatTimeMixed(safeKillTime) : '0h ago';
const killValueFormatted = total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
const attackerSubscript = killDetails ? getAttackerSubscript(killDetails) : undefined;
const attackerSubscript = getAttackerSubscript(killDetails);
const { victimCorpLogoUrl, victimAllianceLogoUrl, victimShipUrl } = buildVictimImageUrls({
victim_char_id: safeVictimCharId,
victim_ship_type_id: safeVictimShipTypeId,
victim_corp_id: safeVictimCorpId,
victim_alliance_id: safeVictimAllianceId,
victim_char_id,
victim_ship_type_id,
victim_corp_id,
victim_alliance_id,
});
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
final_blow_char_id: safeFinalBlowCharId,
final_blow_corp_id: safeFinalBlowCorpId,
final_blow_alliance_id: safeFinalBlowAllianceId,
final_blow_char_id,
final_blow_corp_id,
final_blow_alliance_id,
});
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
victimAllianceLogoUrl,
victimCorpLogoUrl,
safeVictimAllianceName,
safeVictimCorpName,
victim_alliance_name,
victim_corp_name,
'Victim',
);
@@ -111,25 +87,25 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
attackerIsNpc,
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
safeFinalBlowAllianceName,
safeFinalBlowCorpName,
safeFinalBlowShipTypeId,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
),
[
attackerAllianceLogoUrl,
attackerCorpLogoUrl,
attackerIsNpc,
safeFinalBlowAllianceName,
safeFinalBlowCorpName,
safeFinalBlowShipTypeId,
final_blow_alliance_name,
final_blow_corp_name,
final_blow_ship_type_id,
],
);
// Define attackerTicker to use the alliance ticker if available, otherwise the corp ticker.
const attackerTicker = attackerIsNpc ? '' : safeFinalBlowAllianceTicker || safeFinalBlowCorpTicker || '';
const attackerTicker = attackerIsNpc ? '' : final_blow_alliance_ticker || final_blow_corp_ticker || '';
// For the attacker image link: if the attacker is not an NPC, link to the character page; otherwise, link to the kill page.
const attackerLink = attackerIsNpc ? zkillLink('kill', safeKillmailId) : zkillLink('character', safeFinalBlowCharId);
const attackerLink = attackerIsNpc ? zkillLink('kill', killmail_id) : zkillLink('character', final_blow_char_id);
return (
<div
@@ -145,7 +121,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victimShipUrl && (
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
<a
href={zkillLink('kill', safeKillmailId)}
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="block w-full h-full"
@@ -161,7 +137,7 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
{victimPrimaryLogoUrl && (
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
<a
href={zkillLink('kill', safeKillmailId)}
href={zkillLink('kill', killmail_id)}
target="_blank"
rel="noopener noreferrer"
className="relative block shrink-0 w-8 h-8 overflow-hidden"
@@ -177,12 +153,12 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
</div>
<div className="flex flex-col ml-2 flex-1 min-w-0 overflow-hidden leading-[1rem]">
<div className="truncate text-stone-200">
{safeVictimCharName}
{victim_char_name}
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
</div>
<div className="truncate text-stone-300 flex items-center gap-1">
<span className="text-stone-400 overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
{safeVictimShipName}
{victim_ship_name}
</span>
{killValueFormatted && (
<>
@@ -194,9 +170,9 @@ export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, classNam
</div>
<div className="flex items-center ml-auto gap-2">
<div className="flex flex-col items-end flex-1 min-w-0 overflow-hidden text-right leading-[1rem]">
{!attackerIsNpc && (safeFinalBlowCharName || attackerTicker) && (
{!attackerIsNpc && (final_blow_char_name || attackerTicker) && (
<div className="truncate text-stone-200">
{safeFinalBlowCharName}
{final_blow_char_name}
{!attackerIsNpc && attackerTicker && <span className="ml-1 text-stone-400">/ {attackerTicker}</span>}
</div>
)}

View File

@@ -33,10 +33,7 @@ export function formatISK(value: number): string {
return Math.round(value).toString();
}
export function getAttackerSubscript(kill: DetailedKill | undefined) {
if (!kill) {
return null;
}
export function getAttackerSubscript(kill: DetailedKill) {
if (kill.npc) {
return { label: 'npc', cssClass: 'text-purple-400' };
}

View File

@@ -13,13 +13,16 @@ interface UseSystemKillsProps {
sinceHours?: number;
}
function combineKills(existing: DetailedKill[], incoming: DetailedKill[]): DetailedKill[] {
// Don't filter by time when storing - let components filter when displaying
function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] {
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
const byId: Record<string, DetailedKill> = {};
for (const kill of [...existing, ...incoming]) {
if (!kill.kill_time) continue;
byId[kill.killmail_id] = kill;
const killTimeMs = new Date(kill.kill_time).valueOf();
if (killTimeMs >= cutoff) {
byId[kill.killmail_id] = kill;
}
}
return Object.values(byId);
@@ -52,14 +55,14 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
for (const [sid, newKills] of Object.entries(killsMap)) {
const existing = updated[sid] ?? [];
const combined = combineKills(existing, newKills);
const combined = combineKills(existing, newKills, effectiveSinceHours);
updated[sid] = combined;
}
return { ...prev, detailedKills: updated };
});
},
[update],
[update, effectiveSinceHours],
);
const fetchKills = useCallback(

View File

@@ -4,6 +4,3 @@ export * from './RoutesWidget';
export * from './SystemSignatures';
export * from './SystemStructures';
export * from './WSystemKills';
export * from './WRoutesUser';
export * from './WRoutesPublic';
export * from './CommentsWidget';

View File

@@ -13,16 +13,12 @@ import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandle
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { Commands } from '@/hooks/Mapper/types';
import { PingsInterface } from '@/hooks/Mapper/components/mapInterface/components';
export interface MapRootContentProps {}
// eslint-disable-next-line no-empty-pattern
export const MapRootContent = ({}: MapRootContentProps) => {
const {
storedSettings: { interfaceSettings },
data,
} = useMapRootState();
const { interfaceSettings, data } = useMapRootState();
const { isShowMenu } = interfaceSettings;
const { showCharacterActivity } = data;
const { handleHideCharacterActivity } = useCharacterActivityHandlers();
@@ -63,21 +59,17 @@ export const MapRootContent = ({}: MapRootContentProps) => {
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
additionalContent={<PingsInterface hasLeftOffset />}
/>
</div>
</div>
) : (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar>
<div className="flex items-center ml-1">
<PingsInterface />
<MapContextMenu
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
/>
</div>
<MapContextMenu
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
/>
</Topbar>
{mapInterface}
</div>

View File

@@ -8,15 +8,10 @@ interface CharacterActivityProps {
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
return (
<Dialog
header="Character Activity"
visible={visible}
className="w-[550px] max-h-[90vh]"
onHide={onHide}
dismissableMask
contentClassName="p-0 h-full flex flex-col"
>
<CharacterActivityContent />
<Dialog header="Character Activity" visible={visible} className="w-[550px]" onHide={onHide} dismissableMask>
<div className="w-full h-[500px] flex flex-col overflow-hidden p-0 m-0">
<CharacterActivityContent />
</div>
</Dialog>
);
};

View File

@@ -31,49 +31,41 @@ export const CharacterActivityContent = () => {
}
return (
<div className="w-full h-full overflow-auto custom-scrollbar">
<DataTable
value={activity}
scrollable
className="w-full"
tableClassName="w-full border-0"
emptyMessage="No character activity data available"
sortField="passages"
sortOrder={-1}
size="small"
rowClassName={getRowClassName}
rowHover
>
<Column
field="character_name"
header="Character"
body={renderCharacterTemplate}
sortable
className="!py-[6px]"
/>
<DataTable
value={activity}
scrollable
className="w-full"
tableClassName="w-full border-0"
emptyMessage="No character activity data available"
sortField="passages"
sortOrder={-1}
size="small"
rowClassName={getRowClassName}
rowHover
>
<Column field="character_name" header="Character" body={renderCharacterTemplate} sortable className="!py-[6px]" />
<Column
field="passages"
header="Passages"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'passages')}
sortable
/>
<Column
field="connections"
header="Connections"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'connections')}
sortable
/>
<Column
field="signatures"
header="Signatures"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'signatures')}
sortable
/>
</DataTable>
</div>
<Column
field="passages"
header="Passages"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'passages')}
sortable
/>
<Column
field="connections"
header="Connections"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'connections')}
sortable
/>
<Column
field="signatures"
header="Signatures"
headerClassName="[&_.p-column-header-content]:justify-center"
body={rowData => renderValueTemplate(rowData, 'signatures')}
sortable
/>
</DataTable>
);
};

View File

@@ -15,10 +15,7 @@ export interface MapContextMenuProps {
}
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
const {
outCommand,
storedSettings: { setInterfaceSettings },
} = useMapRootState();
const { outCommand, setInterfaceSettings } = useMapRootState();
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);

View File

@@ -4,7 +4,13 @@ import { useCallback, useRef, useState } from 'react';
import { TabPanel, TabView } from 'primereact/tabview';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { CONNECTIONS_CHECKBOXES_PROPS, SIGNATURES_CHECKBOXES_PROPS, SYSTEMS_CHECKBOXES_PROPS } from './constants.ts';
import {
CONNECTIONS_CHECKBOXES_PROPS,
SIGNATURES_CHECKBOXES_PROPS,
SYSTEMS_CHECKBOXES_PROPS,
THEME_SETTING,
UI_CHECKBOXES_PROPS,
} from './constants.ts';
import {
MapSettingsProvider,
useMapSettings,
@@ -28,8 +34,6 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
refVars.current = { outCommand, onHide, visible };
const handleShow = useCallback(async () => {
// TODO: need fix it - add type?
// @ts-ignore
const { user_settings } = await refVars.current.outCommand({
type: OutCommand.getUserSettings,
data: null,
@@ -84,9 +88,17 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
{renderSettingsList(UI_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
<WidgetsSettings />
</TabPanel>
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
{renderSettingItem(THEME_SETTING)}
</TabPanel>
</TabView>
</div>
</div>

View File

@@ -31,16 +31,13 @@ type MapSettingsContextType = {
const MapSettingsContext = createContext<MapSettingsContextType | undefined>(undefined);
export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
const {
outCommand,
storedSettings: { interfaceSettings, setInterfaceSettings },
} = useMapRootState();
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
...DEFAULT_REMOTE_SETTINGS,
});
const mergedSettings: UserSettings = useMemo(() => {
const mergedSettings = useMemo(() => {
return {
...userRemoteSettings,
...interfaceSettings,
@@ -78,7 +75,7 @@ export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
if (item.type === 'checkbox') {
return (
<PrettySwitchbox
key={item.prop.toString()}
key={item.prop}
label={item.label}
checked={!!currentValue}
setChecked={checked => handleSettingChange(item.prop, checked)}
@@ -88,9 +85,8 @@ export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
if (item.type === 'dropdown' && item.options) {
return (
<div key={item.prop.toString()} className="grid grid-cols-[auto_1fr_auto] items-center">
<label className="text-[var(--gray-200)] text-[13px] select-none">{item.label}:</label>
<div className="border-b-2 border-dotted border-[#3f3f3f] h-px mx-3" />
<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}

View File

@@ -1,34 +1,13 @@
import {
MINI_MAP_PLACEMENT,
PINGS_PLACEMENT,
THEME_SETTING,
UI_CHECKBOXES_PROPS,
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
import { COMMON_CHECKBOXES_PROPS } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
import { SettingsListItem } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
import { useCallback } from 'react';
export const CommonSettings = () => {
const { renderSettingItem } = useMapSettings();
const renderSettingsList = useCallback(
(list: SettingsListItem[]) => {
return list.map(renderSettingItem);
},
[renderSettingItem],
);
const renderSettingsList = (list: SettingsListItem[]) => {
return list.map(renderSettingItem);
};
return (
<div className="flex flex-col h-full gap-1">
<div>
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(UI_CHECKBOXES_PROPS)}</div>
</div>
<div className="border-b-2 border-dotted border-stone-700/50 h-px my-3" />
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(MINI_MAP_PLACEMENT)}</div>
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(PINGS_PLACEMENT)}</div>
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(THEME_SETTING)}</div>
</div>
);
return <div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>;
};

View File

@@ -10,9 +10,9 @@ interface PrettySwitchboxProps {
export const PrettySwitchbox = ({ checked, setChecked, label }: PrettySwitchboxProps) => {
return (
<label className="grid grid-cols-[auto_1fr_auto] items-center">
<span className="text-[var(--gray-200)] text-[13px] select-none">{label}</span>
<div className="border-b-2 border-dotted border-[#3f3f3f] h-px mx-3" />
<label className={styles.CheckboxContainer}>
<span>{label}</span>
<div />
<div className={styles.smallInputSwitch}>
<WdCheckbox size="m" label={''} value={checked} onChange={e => setChecked(e.checked ?? false)} />
</div>

View File

@@ -1,6 +1,5 @@
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
import { InterfaceStoredSettingsProps } from '@/hooks/Mapper/mapRootProvider';
import { AvailableThemes, MiniMapPlacement, PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { AvailableThemes, InterfaceStoredSettingsProps } from '@/hooks/Mapper/mapRootProvider';
export const DEFAULT_REMOTE_SETTINGS = {
[UserSettingsRemoteProps.link_signature_on_splash]: false,
@@ -14,13 +13,13 @@ export const UserSettingsRemoteList = [
UserSettingsRemoteProps.delete_connection_with_sigs,
];
// export const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
// // {
// // prop: InterfaceStoredSettingsProps.isShowMinimap,
// // label: 'Show Minimap',
// // type: 'checkbox',
// // },
// ];
export const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
{
prop: InterfaceStoredSettingsProps.isShowMinimap,
label: 'Show Minimap',
type: 'checkbox',
},
];
export const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [
{
@@ -90,32 +89,3 @@ export const THEME_SETTING: SettingsListItem = {
type: 'dropdown',
options: THEME_OPTIONS,
};
export const MINI_MAP_PLACEMENT_OPTIONS = [
{ label: 'Right Bottom', value: MiniMapPlacement.rightBottom },
{ label: 'Right Top', value: MiniMapPlacement.rightTop },
{ label: 'Left Top', value: MiniMapPlacement.leftTop },
{ label: 'Left Bottom', value: MiniMapPlacement.leftBottom },
{ label: 'Hide', value: MiniMapPlacement.hide },
];
export const MINI_MAP_PLACEMENT: SettingsListItem = {
prop: 'minimapPlacement',
label: 'Minimap Placement',
type: 'dropdown',
options: MINI_MAP_PLACEMENT_OPTIONS,
};
export const PINGS_PLACEMENT_OPTIONS = [
{ label: 'Right Top', value: PingsPlacement.rightTop },
{ label: 'Left Top', value: PingsPlacement.leftTop },
{ label: 'Left Bottom', value: PingsPlacement.leftBottom },
{ label: 'Right Bottom', value: PingsPlacement.rightBottom },
];
export const PINGS_PLACEMENT: SettingsListItem = {
prop: 'pingsPlacement',
label: 'Pings Placement',
type: 'dropdown',
options: PINGS_PLACEMENT_OPTIONS,
};

View File

@@ -1,4 +1,4 @@
import { InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
export enum UserSettingsRemoteProps {
link_signature_on_splash = 'link_signature_on_splash',

View File

@@ -3,7 +3,7 @@
}
.SidebarOnTheMap {
width: 500px;
width: 400px;
padding: 0 !important;
:global {

View File

@@ -1,17 +1,15 @@
import classes from './OnTheMap.module.scss';
import { Sidebar } from 'primereact/sidebar';
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
import clsx from 'clsx';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { CharacterCard, TooltipPosition, WdCheckbox, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { CharacterCard, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
import useLocalStorageState from 'use-local-storage-state';
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
type WindowLocalSettingsType = {
compact: boolean;
@@ -35,7 +33,7 @@ const itemTemplate = (item: CharacterTypeRaw & WithIsOwnCharacter, options: Virt
})}
style={{ height: options.props.itemSize + 'px' }}
>
<CharacterCard showCorporationLogo showAllyLogo showSystem showTicker showShip {...item} />
<CharacterCard showSystem {...item} />
</div>
);
};
@@ -50,8 +48,6 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
data: { characters, userCharacters },
} = useMapRootState();
const [searchVal, setSearchVal] = useState('');
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:onTheMap:settings', {
defaultValue: STORED_DEFAULT_VALUES,
});
@@ -65,54 +61,13 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
);
const sorted = useMemo(() => {
let out = characters.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id) })).sort(sortCharacters);
if (searchVal !== '') {
out = out.filter(x => {
const normalized = searchVal.toLowerCase();
if (x.name.toLowerCase().includes(normalized)) {
return true;
}
if (x.corporation_name.toLowerCase().includes(normalized)) {
return true;
}
if (x.alliance_name?.toLowerCase().includes(normalized)) {
return true;
}
if (x.corporation_ticker.toLowerCase().includes(normalized)) {
return true;
}
if (x.alliance_ticker?.toLowerCase().includes(normalized)) {
return true;
}
if (x.ship?.ship_name?.toLowerCase().includes(normalized)) {
return true;
}
if (x.ship?.ship_type_info.name?.toLowerCase().includes(normalized)) {
return true;
}
if (x.ship?.ship_type_info.group_name?.toLowerCase().includes(normalized)) {
return true;
}
return false;
});
}
const out = characters.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id) })).sort(sortCharacters);
if (showOffline && !settings.hideOffline) {
return out;
}
return out.filter(x => x.online);
}, [showOffline, searchVal, characters, settings.hideOffline, userCharacters]);
}, [showOffline, characters, settings.hideOffline, userCharacters]);
return (
<Sidebar
@@ -124,30 +79,7 @@ export const OnTheMap = ({ show, onHide }: OnTheMapProps) => {
icons={<></>}
>
<div className={clsx(classes.SidebarContent, '')}>
<div className={'flex justify-between items-center gap-2 px-2 pt-1'}>
<IconField>
{searchVal.length > 0 && (
<WdImgButton
className="pi pi-trash"
textSize={WdImageSize.large}
tooltip={{
content: 'Clear',
className: 'pi p-input-icon',
position: TooltipPosition.top,
}}
onClick={() => setSearchVal('')}
/>
)}
<InputText
id="label"
aria-describedby="label"
autoComplete="off"
value={searchVal}
placeholder="Type to search"
onChange={e => setSearchVal(e.target.value)}
/>
</IconField>
<div className={'flex justify-end items-center gap-2 px-3'}>
{showOffline && (
<WdCheckbox
size="m"

View File

@@ -1,6 +1,6 @@
import classes from './RightBar.module.scss';
import clsx from 'clsx';
import { ReactNode, useCallback } from 'react';
import { useCallback } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
@@ -12,21 +12,22 @@ interface RightBarProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
additionalContent?: ReactNode;
}
export const RightBar = ({
onShowOnTheMap,
onShowMapSettings,
onShowTrackingDialog,
additionalContent,
}: RightBarProps) => {
const {
storedSettings: { interfaceSettings, setInterfaceSettings },
} = useMapRootState();
export const RightBar = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: RightBarProps) => {
const { interfaceSettings, setInterfaceSettings } = useMapRootState();
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
const toggleMinimap = useCallback(() => {
setInterfaceSettings(x => ({
...x,
isShowMinimap: !x.isShowMinimap,
}));
}, [setInterfaceSettings]);
const toggleKSpace = useCallback(() => {
setInterfaceSettings(x => ({
...x,
@@ -75,7 +76,6 @@ export const RightBar = ({
</WdTooltipWrapper>
</>
)}
{additionalContent}
</div>
<div className="flex flex-col items-center mb-2 gap-1">
@@ -104,6 +104,16 @@ export const RightBar = ({
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content={isShowMinimap ? 'Hide minimap' : 'Show minimap'} position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={toggleMinimap}
>
<i className={isShowMinimap ? 'pi pi-eye' : 'pi pi-eye-slash'}></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Switch to menu" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"

View File

@@ -31,7 +31,6 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
const signatureForm = useForm<Partial<SystemSignaturePrepared>>({});
const handleSave = useCallback(
// TODO: need fix
async (e: any) => {
e?.preventDefault();
if (!signatureData) {
@@ -53,7 +52,6 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
},
});
// TODO: need fix
if (values.isEOL) {
await outCommand({
type: OutCommand.updateConnectionTimeStatus,
@@ -67,8 +65,8 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
if (values.type) {
const whShipSize = getWhSize(wormholes, values.type);
if (whShipSize !== undefined && whShipSize !== null) {
await outCommand({
if (whShipSize) {
outCommand({
type: OutCommand.updateConnectionShipSizeType,
data: {
source: systemId,
@@ -83,9 +81,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
out = {
...out,
custom_info: JSON.stringify({
// TODO: need fix
k162Type: values.k162Type,
// TODO: need fix
isEOL: values.isEOL,
}),
};
@@ -149,7 +145,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
signatureForm.reset();
onHide();
},
[signatureData, signatureForm, outCommand, systemId, onHide, wormholes],
[signatureForm, onHide, outCommand, signatureData, systemId],
);
useEffect(() => {
@@ -170,7 +166,6 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
signatureForm.reset({
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
// TODO: need fix
k162Type: k162Type,
isEOL: isEOL,
...rest,

View File

@@ -44,7 +44,7 @@ export const TrackingCharactersList = () => {
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
headerClassName="[&_div]:ml-2"
body={row => {
return <CharacterCard showCorporationLogo showTicker isOwn {...row.character} />;
return <CharacterCard showShipName={false} showSystem={false} isOwn {...row.character} />;
}}
/>
</DataTable>

View File

@@ -10,8 +10,8 @@ const renderValCharacterTemplate = (row: TrackingCharacter | undefined) => {
}
return (
<div className="py-1 w-full">
<CharacterCard compact isOwn {...row.character} />
<div className="py-1">
<CharacterCard compact showShipName={false} showSystem={false} isOwn {...row.character} />
</div>
);
};
@@ -21,11 +21,7 @@ const renderCharacterTemplate = (row: TrackingCharacter | undefined) => {
return <div className="h-[33px] flex items-center">Character is not selected</div>;
}
return (
<div className="w-full">
<CharacterCard isOwn {...row.character} />
</div>
);
return <CharacterCard showShipName={false} showSystem={false} isOwn {...row.character} />;
};
export const TrackingSettings = () => {

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { ActivitySummary } from '@/hooks/Mapper/types';
import type { ActivitySummary } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivity';
/**
* Hook for character activity related handlers

View File

@@ -1,5 +1,5 @@
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
@@ -10,6 +10,7 @@ import {
SystemLinkSignatureDialog,
SystemSettingsDialog,
} from '@/hooks/Mapper/components/mapInterface/components';
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';
@@ -19,6 +20,7 @@ import { Node, useReactFlow, XYPosition } from 'reactflow';
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
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 {
@@ -26,52 +28,33 @@ import {
SearchOnSubmitCallback,
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
import { useHotkey } from '../../hooks/useHotkey';
import { PingType } from '@/hooks/Mapper/types/ping.ts';
import { SystemPingDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemPingDialog';
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { MINIMAP_PLACEMENT_MAP } from '@/hooks/Mapper/constants.ts';
import type { PanelPosition } from '@reactflow/core';
import { MINI_MAP_PLACEMENT_OFFSETS } from './constants.ts';
// TODO: INFO - this component needs for abstract work with Map instance
export const MapWrapper = () => {
const {
update,
outCommand,
data: {
pings,
selectedConnections,
selectedSystems,
hubs,
userHubs,
systems,
linkSignatureToSystem,
systemSignatures,
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem, systemSignatures },
interfaceSettings: {
isShowMenu,
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
isShowKSpace,
isThickConnections,
isShowBackgroundPattern,
isShowUnsplashedSignatures,
isSoftBackground,
theme,
},
storedSettings: { interfaceSettings },
} = useMapRootState();
const {
isShowMenu,
isShowKSpace,
isThickConnections,
isShowBackgroundPattern,
isShowUnsplashedSignatures,
isSoftBackground,
theme,
minimapPlacement,
} = interfaceSettings;
const { deleteSystems } = useDeleteSystems();
const { mapRef, runCommand } = useCommonMapEventProcessor();
const { getNodes } = useReactFlow();
const { updateLinkSignatureToSystem } = useCommandsSystems();
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, userHubs, outCommand });
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
const { handleSystemMultipleContext, ...systemMultipleCtxProps } = useContextMenuSystemMultipleHandlers();
const [openSettings, setOpenSettings] = useState<string | null>(null);
const [openPing, setOpenPing] = useState<{ type: PingType; solar_system_id: string } | null>(null);
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
@@ -113,8 +96,6 @@ export const MapWrapper = () => {
event => {
switch (event.type) {
case OutCommand.openSettings:
// TODO - need fix it
// @ts-ignore
setOpenSettings(event.data.system_id);
break;
default:
@@ -126,34 +107,28 @@ export const MapWrapper = () => {
[outCommand],
);
const handleSystemContextMenu = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(ev: any, systemId: string) => {
const { selectedSystems, systems } = ref.current;
if (selectedSystems.length > 1) {
const systemsInfo: Node[] = selectedSystems.map(x => ({ data: getSystemById(systems, x), id: x }) as Node);
const handleSystemContextMenu = useCallback((ev: any, systemId: string) => {
const { selectedSystems, systems } = ref.current;
if (selectedSystems.length > 1) {
const systemsInfo: Node[] = selectedSystems.map(x => ({ data: getSystemById(systems, x), id: x }) as Node);
handleSystemMultipleContext(ev, systemsInfo);
return;
}
handleSystemMultipleContext(ev, systemsInfo);
return;
}
open(ev, systemId);
},
[handleSystemMultipleContext, open],
);
open(ev, systemId);
}, []);
const handleConnectionDbClick = useCallback((e: SolarSystemConnection) => setSelectedConnection(e), []);
const handleDeleteSelected = useCallback(() => {
const restDel = getNodes()
.filter(x => x.selected && !x.data.locked)
.filter(x => !pings.some(p => x.data.id === p.solar_system_id))
.map(x => x.data.id);
if (restDel.length > 0) {
ref.current.deleteSystems(restDel);
}
}, [getNodes, pings]);
}, [getNodes]);
const onAddSystem: OnMapAddSystemCallback = useCallback(({ coordinates }) => {
setOpenAddSystem(coordinates);
@@ -177,27 +152,6 @@ export const MapWrapper = () => {
[openAddSystem, outCommand],
);
const handleOpenSettings = useCallback(() => {
ref.current.systemContextProps.systemId && setOpenSettings(ref.current.systemContextProps.systemId);
}, []);
const handleTogglePing = useCallback(async (type: PingType, solar_system_id: string, hasPing: boolean) => {
if (hasPing) {
await outCommand({
type: OutCommand.cancelPing,
data: { type, solar_system_id: solar_system_id },
});
return;
}
setOpenPing({ type, solar_system_id });
}, []);
const handleCustomLabelDialog = useCallback(() => {
const { systemContextProps } = ref.current;
systemContextProps.systemId && setOpenCustomLabel(systemContextProps.systemId);
}, []);
useHotkey(false, ['Delete'], (event: KeyboardEvent) => {
const targetWindow = (event.target as HTMLHtmlElement)?.closest(`[data-window-id="${MAP_ROOT_ID}"]`);
@@ -219,22 +173,6 @@ export const MapWrapper = () => {
outCommand({ type: OutCommand.loadSignatures, data: {} });
}, [isShowUnsplashedSignatures, systems]);
const { showMinimap, minimapPosition, minimapClasses } = useMemo(() => {
const rawPlacement = minimapPlacement == null ? MiniMapPlacement.rightBottom : minimapPlacement;
if (rawPlacement === MiniMapPlacement.hide) {
return { minimapPosition: undefined, showMinimap: false, minimapClasses: '' };
}
const mmClasses = MINI_MAP_PLACEMENT_OFFSETS[rawPlacement];
return {
minimapPosition: MINIMAP_PLACEMENT_MAP[rawPlacement] as PanelPosition,
showMinimap: true,
minimapClasses: isShowMenu ? mmClasses.default : mmClasses.withLeftMenu,
};
}, [minimapPlacement, isShowMenu]);
return (
<>
<Map
@@ -244,29 +182,19 @@ export const MapWrapper = () => {
onConnectionInfoClick={handleConnectionDbClick}
onSystemContextMenu={handleSystemContextMenu}
onSelectionContextMenu={handleSystemMultipleContext}
minimapClasses={minimapClasses}
isShowMinimap={showMinimap}
minimapClasses={!isShowMenu ? classes.MiniMap : undefined}
isShowMinimap={isShowMinimap}
showKSpaceBG={isShowKSpace}
isThickConnections={isThickConnections}
isShowBackgroundPattern={isShowBackgroundPattern}
isSoftBackground={isSoftBackground}
theme={theme}
pings={pings}
onAddSystem={onAddSystem}
minimapPlacement={minimapPosition}
/>
{openSettings != null && (
<SystemSettingsDialog systemId={openSettings} visible setVisible={() => setOpenSettings(null)} />
)}
{openPing != null && (
<SystemPingDialog
systemId={openPing.solar_system_id}
type={openPing.type}
visible
setVisible={() => setOpenPing(null)}
/>
)}
{openCustomLabel != null && (
<SystemCustomLabelDialog systemId={openCustomLabel} visible setVisible={() => setOpenCustomLabel(null)} />
@@ -287,11 +215,14 @@ export const MapWrapper = () => {
<ContextMenuSystem
systems={systems}
hubs={hubs}
userHubs={userHubs}
{...systemContextProps}
onOpenSettings={handleOpenSettings}
onTogglePing={handleTogglePing}
onCustomLabelDialog={handleCustomLabelDialog}
onOpenSettings={() => {
systemContextProps.systemId && setOpenSettings(systemContextProps.systemId);
}}
onCustomLabelDialog={() => {
const { systemContextProps } = ref.current;
systemContextProps.systemId && setOpenCustomLabel(systemContextProps.systemId);
}}
/>
<ContextMenuSystemMultiple {...systemMultipleCtxProps} />

View File

@@ -1,8 +0,0 @@
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const MINI_MAP_PLACEMENT_OFFSETS = {
[MiniMapPlacement.rightTop]: { default: '!top-[48px]', withLeftMenu: '!top-[48px] !right-[58px]' },
[MiniMapPlacement.rightBottom]: { default: '!bottom-[0px]', withLeftMenu: '!bottom-[0px] !right-[58px]' },
[MiniMapPlacement.leftTop]: { default: '!top-[48px] !left-[56px]', withLeftMenu: '!top-[48px] !left-[56px]' },
[MiniMapPlacement.leftBottom]: { default: '!left-[56px] !bottom-[0px]', withLeftMenu: '!left-[56px] !bottom-[0px]' },
};

View File

@@ -1,8 +1,7 @@
import { MapEvent } from '@/hooks/Mapper/events';
// import { useThrottle } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { Command, Commands, MapHandlers } from '@/hooks/Mapper/types';
import { MutableRefObject, useCallback, useEffect, useRef } from 'react';
import { Command, Commands, MapHandlers } from '@/hooks/Mapper/types';
import { MapEvent } from '@/hooks/Mapper/events';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useCommonMapEventProcessor = () => {
const mapRef = useRef<MapHandlers>() as MutableRefObject<MapHandlers>;
@@ -12,14 +11,13 @@ export const useCommonMapEventProcessor = () => {
const refQueue = useRef<MapEvent<Command>[]>([]);
// const ref = useRef({})
const runCommand = useCallback(({ name, data }: MapEvent<Command>) => {
switch (name) {
case Commands.addSystems:
case Commands.removeSystems:
// case Commands.updateSystems:
// case Commands.addConnections:
// case Commands.removeConnections:
// case Commands.updateConnection:
refQueue.current.push({ name, data });
return;
}
@@ -28,17 +26,9 @@ export const useCommonMapEventProcessor = () => {
mapRef.current?.command(name, data);
}, []);
const processQueue = useCallback(() => {
const commands = [...refQueue.current];
refQueue.current = [];
commands.forEach(x => mapRef.current?.command(x.name, x.data));
}, []);
// const throttledProcessQueue = useThrottle(processQueue, 200);
useEffect(() => {
// throttledProcessQueue();
processQueue();
refQueue.current.forEach(x => mapRef.current?.command(x.name, x.data));
refQueue.current = [];
}, [systems]);
return {

View File

@@ -1,9 +1,9 @@
import { sortOnlineFunc } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import clsx from 'clsx';
import { useMemo } from 'react';
import { Characters } from '../characters/Characters';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo } from 'react';
import clsx from 'clsx';
import { sortOnlineFunc } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
const Topbar = ({ children }: WithChildren) => {
const {
@@ -24,10 +24,7 @@ const Topbar = ({ children }: WithChildren) => {
>
<span className="flex-1"></span>
<span className="mr-2"></span>
<div className="flex gap-1 items-center">
<Characters data={charsToShow} />
</div>
<Characters data={charsToShow} />
{children}
</nav>
);

View File

@@ -1,34 +1,20 @@
import {
TooltipPosition,
WdEveEntityPortrait,
WdEveEntityPortraitSize,
WdEveEntityPortraitType,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
import { Commands } from '@/hooks/Mapper/types/mapHandlers';
import clsx from 'clsx';
import { useCallback } from 'react';
import clsx from 'clsx';
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
import { Commands } from '@/hooks/Mapper/types/mapHandlers';
import { emitMapEvent } from '@/hooks/Mapper/events';
import { CharacterPortrait, CharacterPortraitSize } from '@/hooks/Mapper/components/ui-kit';
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
import classes from './CharacterCard.module.scss';
export type CharacterCardProps = {
type CharacterCardProps = {
compact?: boolean;
showSystem?: boolean;
showTicker?: boolean;
showShip?: boolean;
showShipName?: boolean;
useSystemsCache?: boolean;
showCorporationLogo?: boolean;
showAllyLogo?: boolean;
simpleMode?: boolean;
} & WithIsOwnCharacter &
WithClassName;
type CharacterCardInnerProps = CharacterCardProps & CharacterTypeRaw;
} & CharacterTypeRaw &
WithIsOwnCharacter;
const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => {
@@ -39,19 +25,13 @@ export const getShipName = (name: string) => {
};
export const CharacterCard = ({
simpleMode,
compact = false,
isOwn,
showSystem,
showShip,
showShipName,
showCorporationLogo,
showAllyLogo,
showTicker,
useSystemsCache,
className,
...char
}: CharacterCardInnerProps) => {
}: CharacterCardProps) => {
const handleSelect = useCallback(() => {
emitMapEvent({
name: Commands.centerSystem,
@@ -64,128 +44,28 @@ export const CharacterCard = ({
const shipType = char.ship?.ship_type_info?.name;
const locationShown = showSystem && char.location?.solar_system_id;
// INFO: Simple mode show only name and icon of ally/corp. By default it compact view
if (simpleMode) {
return (
<div className={clsx('text-xs box-border', className)} onClick={handleSelect}>
<div className="flex items-center gap-1 relative">
<WdEveEntityPortrait eveId={char.eve_id} size={WdEveEntityPortraitSize.w18} />
{!char.alliance_id && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.corporation_name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.corporation}
eveId={char.corporation_id?.toString()}
size={WdEveEntityPortraitSize.w18}
/>
</WdTooltipWrapper>
)}
{char.alliance_id && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.alliance_name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.alliance}
eveId={char.alliance_id?.toString()}
size={WdEveEntityPortraitSize.w18}
/>
</WdTooltipWrapper>
)}
<div className="flex overflow-hidden text-left">
<div className="flex">
<span
className={clsx(
'overflow-hidden text-ellipsis whitespace-nowrap',
isOwn ? 'text-orange-400' : 'text-gray-200',
)}
title={char.name}
>
{char.name}
</span>
{showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>}
</div>
</div>
</div>
</div>
);
}
if (compact) {
return (
<div className={clsx('text-xs box-border w-full', className)} onClick={handleSelect}>
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
<div className="w-full flex items-center gap-1 relative">
<WdEveEntityPortrait eveId={char.eve_id} size={WdEveEntityPortraitSize.w18} />
{showCorporationLogo && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.corporation_name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.corporation}
eveId={char.corporation_id?.toString()}
size={WdEveEntityPortraitSize.w18}
/>
</WdTooltipWrapper>
)}
{showAllyLogo && char.alliance_id && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.alliance_name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.alliance}
eveId={char.alliance_id?.toString()}
size={WdEveEntityPortraitSize.w18}
/>
</WdTooltipWrapper>
)}
<CharacterPortrait characterEveId={char.eve_id} size={CharacterPortraitSize.w18} />
{isDocked(char.location) && <span className={classes.Docked} />}
<div className="flex flex-grow-[2] overflow-hidden text-left w-[50px]">
<div className="flex min-w-0">
<span
className={clsx(
'overflow-hidden text-ellipsis whitespace-nowrap',
isOwn ? 'text-orange-400' : 'text-gray-200',
)}
title={char.name}
>
{char.name}
<div className="flex flex-grow overflow-hidden text-left">
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>{char.name}</span>{' '}
<span className="text-gray-400">
{!locationShown && showShipName && shipNameText ? `- ${shipNameText}` : `[${tickerText}]`}
</span>
{showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>}
</div>
</div>
{showShip && shipType && (
<>
{!showShipName && (
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
style={{ maxWidth: '120px' }}
title={shipType}
>
{shipType}
</div>
)}
{showShipName && (
<div className="flex flex-grow-[1] justify-end w-[50px]">
<div className="min-w-0">
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
style={{ maxWidth: '120px' }}
title={shipNameText}
>
{shipNameText}
</div>
</div>
</div>
)}
{char.ship && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.ship.ship_type_info?.name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.ship}
eveId={char.ship.ship_type_id?.toString()}
size={WdEveEntityPortraitSize.w18}
/>
</WdTooltipWrapper>
)}
</>
{shipType && (
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
style={{ maxWidth: '120px' }}
title={shipType}
>
{shipType}
</div>
)}
</div>
</div>
@@ -195,41 +75,11 @@ export const CharacterCard = ({
return (
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
<div className="w-full flex items-center gap-2">
<div className="flex items-center gap-1">
<WdEveEntityPortrait eveId={char.eve_id} size={WdEveEntityPortraitSize.w33} />
{showCorporationLogo && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.corporation_name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.corporation}
eveId={char.corporation_id?.toString()}
size={WdEveEntityPortraitSize.w33}
/>
</WdTooltipWrapper>
)}
{showAllyLogo && char.alliance_id && (
<WdTooltipWrapper position={TooltipPosition.top} content={char.alliance_name}>
<WdEveEntityPortrait
type={WdEveEntityPortraitType.alliance}
eveId={char.alliance_id?.toString()}
size={WdEveEntityPortraitSize.w33}
/>
</WdTooltipWrapper>
)}
</div>
<div className="flex flex-col flex-grow overflow-hidden w-[50px]">
<div className="flex min-w-0">
<span
className={clsx(
'overflow-hidden text-ellipsis whitespace-nowrap',
isOwn ? 'text-orange-400' : 'text-gray-200',
)}
>
{char.name}
</span>
{showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>}
<CharacterPortrait characterEveId={char.eve_id} size={CharacterPortraitSize.w33} />
<div className="flex flex-col flex-grow overflow-hidden">
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
<span className={clsx(isOwn ? 'text-orange-400' : 'text-gray-200')}>{char.name}</span>{' '}
<span className="text-gray-400">[{tickerText}]</span>
</div>
{locationShown ? (
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
@@ -246,31 +96,16 @@ export const CharacterCard = ({
)
)}
</div>
{showShip && shipType && (
<>
<div className="flex flex-col flex-shrink-0 items-end self-start">
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]"
title={shipType}
>
{shipType}
</div>
<div
className="flex justify-end text-stone-500 overflow-hidden text-ellipsis whitespace-nowrap max-w-[200px]"
title={shipNameText}
>
{shipNameText}
</div>
{shipType && (
<div className="flex-shrink-0 self-start">
<div
className="text-gray-300 overflow-hidden text-ellipsis whitespace-nowrap"
style={{ maxWidth: '200px' }}
title={shipType}
>
{shipType}
</div>
{char.ship && (
<WdEveEntityPortrait
type={WdEveEntityPortraitType.ship}
eveId={char.ship.ship_type_id?.toString()}
size={WdEveEntityPortraitSize.w33}
/>
)}
</>
</div>
)}
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More