mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
.RoutesListRoot {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.RouteSystem {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ffffff;
|
||||
|
||||
cursor: pointer;
|
||||
transition: opacity 200ms;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.Faded {
|
||||
opacity: 0.3;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import classes from './RoutesList.module.scss';
|
||||
import { Route, SystemStaticInfoShort } from '@/hooks/Mapper/types/routes.ts';
|
||||
import clsx from 'clsx';
|
||||
import { SystemViewStandalone, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getBackgroundClass, getShapeClass } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { MouseEvent, useCallback, useRef, useState } from 'react';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export type RouteSystemProps = {
|
||||
destination: number;
|
||||
onClick?(systemId: number): void;
|
||||
onMouseEnter?(systemId: number): void;
|
||||
onMouseLeave?(): void;
|
||||
onContextMenu?(e: MouseEvent, systemId: string): void;
|
||||
faded?: boolean;
|
||||
} & SystemStaticInfoShort;
|
||||
|
||||
export const RouteSystem = ({
|
||||
system_class,
|
||||
security,
|
||||
solar_system_id,
|
||||
class_title,
|
||||
triglavian_invasion_status,
|
||||
solar_system_name,
|
||||
// destination,
|
||||
region_name,
|
||||
faded,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onContextMenu,
|
||||
}: RouteSystemProps) => {
|
||||
const tooltipRef = useRef<WdTooltipHandlers>(null);
|
||||
|
||||
const handleContext = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onContextMenu?.(e, solar_system_id.toString());
|
||||
},
|
||||
[onContextMenu, solar_system_id],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WdTooltip
|
||||
ref={tooltipRef}
|
||||
// targetSelector={`.tooltip-route-sys_${destination}_${solar_system_id}`}
|
||||
content={() => (
|
||||
<SystemViewStandalone
|
||||
security={security}
|
||||
system_class={system_class}
|
||||
class_title={class_title}
|
||||
solar_system_name={solar_system_name}
|
||||
region_name={region_name}
|
||||
solar_system_id={solar_system_id}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
onMouseEnter={e => {
|
||||
tooltipRef.current?.show(e);
|
||||
onMouseEnter?.(solar_system_id);
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
tooltipRef.current?.hide(e);
|
||||
onMouseLeave?.();
|
||||
}}
|
||||
onContextMenu={handleContext}
|
||||
onClick={() => onClick?.(solar_system_id)}
|
||||
className={clsx(
|
||||
classes.RouteSystem,
|
||||
// `tooltip-route-sys_${destination}_${solar_system_id}`,
|
||||
getBackgroundClass(system_class, security),
|
||||
getShapeClass(system_class, triglavian_invasion_status),
|
||||
{ [classes.Faded]: faded },
|
||||
)}
|
||||
></div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface RoutesListProps {
|
||||
onContextMenu?(e: MouseEvent, systemId: string): void;
|
||||
data: Route;
|
||||
}
|
||||
|
||||
export const RoutesList = ({ data, onContextMenu }: RoutesListProps) => {
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const { mapRef } = useMapRootState();
|
||||
|
||||
const handleClick = useCallback(
|
||||
(systemId: number) => mapRef.current?.command(Commands.selectSystem, systemId.toString()),
|
||||
[mapRef],
|
||||
);
|
||||
|
||||
if (!data.has_connection) {
|
||||
return <div className="text-stone-400">No connection</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.RoutesListRoot}>
|
||||
{data.mapped_systems?.filter(Boolean).map(x => {
|
||||
if (!x) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<RouteSystem
|
||||
key={x.solar_system_id}
|
||||
faded={selected !== null && selected !== x?.solar_system_id}
|
||||
destination={data.destination}
|
||||
{...x}
|
||||
onMouseEnter={systemId => {
|
||||
setSelected(systemId);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setSelected(null);
|
||||
}}
|
||||
onContextMenu={onContextMenu}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RoutesList';
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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: () => {},
|
||||
data: { ...DEFAULT_SETTINGS },
|
||||
});
|
||||
|
||||
export const RoutesProvider: React.FC<MapProviderProps> = ({ children }) => {
|
||||
const { update, ref } = useContextStore<RoutesType>(
|
||||
{ ...DEFAULT_SETTINGS },
|
||||
{
|
||||
onAfterAUpdate: values => {
|
||||
localStorage.setItem(SESSION_KEY.routes, JSON.stringify(values));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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<MapContextProps>(RoutesContext);
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
import {
|
||||
RoutesType,
|
||||
useRouteProvider,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { CheckboxChangeEvent } from 'primereact/checkbox';
|
||||
|
||||
interface RoutesSettingsDialog {
|
||||
visible: boolean;
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
type RoutesFlagsType = Omit<RoutesType, 'path_type' | 'avoid'>;
|
||||
|
||||
const checkboxes: { label: string; propName: keyof RoutesFlagsType }[] = [
|
||||
{ label: 'Include Mass Crit', propName: 'include_mass_crit' },
|
||||
{ label: 'Include EOL', propName: 'include_eol' },
|
||||
{ label: 'Include Frigate', propName: 'include_frig' },
|
||||
{ label: 'Include Cruise', propName: 'include_cruise' },
|
||||
{ label: 'Include Thera connections', propName: 'include_thera' },
|
||||
{ label: 'Avoid Wormholes', propName: 'avoid_wormholes' },
|
||||
{ label: 'Avoid Pochven', propName: 'avoid_pochven' },
|
||||
{ label: 'Avoid Edencom systems', propName: 'avoid_edencom' },
|
||||
{ label: 'Avoid Triglavian systems', propName: 'avoid_triglavian' },
|
||||
];
|
||||
|
||||
export const RoutesSettingsDialog = ({ visible, setVisible }: RoutesSettingsDialog) => {
|
||||
const { data, update } = useRouteProvider();
|
||||
|
||||
const [, updateKey] = useState(0);
|
||||
|
||||
const optionsRef = useRef(data);
|
||||
|
||||
const currentData = useRef(data);
|
||||
currentData.current = data;
|
||||
|
||||
const handleChangeEvent = useCallback(
|
||||
(propName: keyof RoutesType) => (event: CheckboxChangeEvent) => {
|
||||
optionsRef.current = { ...optionsRef.current, [propName]: event.checked };
|
||||
updateKey(x => x + 1);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
update({ ...optionsRef.current });
|
||||
setVisible(false);
|
||||
}, [setVisible, update]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
optionsRef.current = currentData.current;
|
||||
updateKey(x => x + 1);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header="Routes settings"
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
style={{ width: '350px' }}
|
||||
onHide={() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
{checkboxes.map(({ label, propName }) => (
|
||||
<WdCheckbox
|
||||
key={propName}
|
||||
label={label}
|
||||
value={optionsRef.current[propName]}
|
||||
onChange={handleChangeEvent(propName)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button onClick={handleSave} outlined size="small" label="Apply"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RoutesSettingsDialog';
|
||||
@@ -0,0 +1,15 @@
|
||||
.RoutesGrid {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
align-items: center;
|
||||
|
||||
column-gap: 3px;
|
||||
row-gap: 2px;
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.RemoveBtn {
|
||||
font-size: 9px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
SystemViewStandalone,
|
||||
TooltipPosition,
|
||||
WdCheckbox,
|
||||
WdImgButton,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
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';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { RoutesSettingsDialog } from './RoutesSettingsDialog';
|
||||
import { RoutesProvider, useRouteProvider } from './RoutesProvider.tsx';
|
||||
import { ContextMenuSystemInfo, useContextMenuSystemInfoHandlers } from '@/hooks/Mapper/components/contexts';
|
||||
|
||||
const sortByDist = (a: Route, b: Route) => {
|
||||
const distA = a.has_connection ? a.systems?.length || 0 : Infinity;
|
||||
const distB = b.has_connection ? b.systems?.length || 0 : Infinity;
|
||||
|
||||
return distA - distB;
|
||||
};
|
||||
|
||||
export const RoutesWidgetContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, hubs = [], systems, routes },
|
||||
mapRef,
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const { loading } = useLoadRoutes();
|
||||
|
||||
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] });
|
||||
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
|
||||
outCommand,
|
||||
hubs,
|
||||
mapRef,
|
||||
});
|
||||
|
||||
const preparedHubs = useMemo(() => {
|
||||
return hubs.map(x => {
|
||||
const sys = getSystemById(systems, x.toString());
|
||||
|
||||
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) };
|
||||
});
|
||||
}, [hubs, systems, systemStatics]);
|
||||
|
||||
const preparedRoutes = useMemo(() => {
|
||||
return (
|
||||
routes?.routes
|
||||
.sort(sortByDist)
|
||||
.filter(x => x.destination.toString() !== systemId)
|
||||
.map(route => ({
|
||||
...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,
|
||||
),
|
||||
) ?? [],
|
||||
})) ?? []
|
||||
);
|
||||
}, [routes?.routes, routes?.systems_static_data, systemId]);
|
||||
|
||||
const refData = useRef({ open, loadSystems });
|
||||
refData.current = { open, loadSystems };
|
||||
|
||||
useEffect(() => {
|
||||
(async () => await refData.current.loadSystems(hubs))();
|
||||
}, [hubs]);
|
||||
|
||||
const handleClick = useCallback((e: MouseEvent, systemId: string) => {
|
||||
refData.current.open(e, systemId);
|
||||
}, []);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
async (e: MouseEvent, systemId: string) => {
|
||||
await refData.current.loadSystems([systemId]);
|
||||
handleClick(e, systemId);
|
||||
},
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center">Loading routes...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!systemId) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
if (hubs.length === 0) {
|
||||
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{systemId !== undefined && routes && (
|
||||
<div className={clsx(classes.RoutesGrid, 'px-2 py-2')}>
|
||||
{preparedRoutes.map(route => {
|
||||
const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2 items-center">
|
||||
<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 }}
|
||||
/>
|
||||
|
||||
<SystemViewStandalone
|
||||
key={route.destination}
|
||||
className={clsx('select-none text-center cursor-context-menu')}
|
||||
hideRegion
|
||||
compact
|
||||
{...sys}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
|
||||
<div className="pl-2 pb-0.5">
|
||||
<RoutesList data={route} onContextMenu={handleContextMenu} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ContextMenuSystemInfo hubs={hubs} systems={systems} systemStatics={systemStatics} {...systemCtxProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutesWidgetComp = () => {
|
||||
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
|
||||
const { data, update } = useRouteProvider();
|
||||
|
||||
const isSecure = data.path_type === 'secure';
|
||||
const handleSecureChange = useCallback(() => {
|
||||
update({
|
||||
...data,
|
||||
path_type: data.path_type === 'secure' ? 'shortest' : 'secure',
|
||||
});
|
||||
}, [data, update]);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full">
|
||||
<span className="select-none">Routes</span>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={'Show shortest'}
|
||||
value={!isSecure}
|
||||
onChange={handleSecureChange}
|
||||
classNameLabel={clsx('text-red-400')}
|
||||
/>
|
||||
<WdImgButton className={PrimeIcons.SLIDERS_H} onClick={() => setRouteSettingsVisible(true)} />
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<RoutesWidgetContent />
|
||||
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutesWidget = () => {
|
||||
return (
|
||||
<RoutesProvider>
|
||||
<RoutesWidgetComp />
|
||||
</RoutesProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useLoadRoutes';
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
RoutesType,
|
||||
useRouteProvider,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export const useLoadRoutes = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { data: routesSettings } = useRouteProvider();
|
||||
|
||||
const {
|
||||
outCommand,
|
||||
data: { selectedSystems, hubs, systems, connections },
|
||||
} = useMapRootState();
|
||||
|
||||
const prevSys = usePrevious(systems);
|
||||
const ref = useRef({ prevSys, selectedSystems });
|
||||
ref.current = { prevSys, selectedSystems };
|
||||
|
||||
const loadRoutes = useCallback(
|
||||
(systemId: string, routesSettings: RoutesType) => {
|
||||
outCommand({
|
||||
type: OutCommand.getRoutes,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
routes_settings: routesSettings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSystems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
loadRoutes(systemId, routesSettings);
|
||||
}, [
|
||||
loadRoutes,
|
||||
selectedSystems,
|
||||
systems?.length,
|
||||
connections,
|
||||
hubs,
|
||||
routesSettings,
|
||||
...Object.keys(routesSettings)
|
||||
.sort()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
.map(x => routesSettings[x]),
|
||||
]);
|
||||
|
||||
return { loading, loadRoutes };
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RoutesWidget.tsx';
|
||||
Reference in New Issue
Block a user