Initial commit

This commit is contained in:
Dmitry Popov
2024-09-18 01:55:30 +04:00
parent 6a96a5f56e
commit 4136aaad76
1675 changed files with 124664 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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>
</>
);
};

View File

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

View File

@@ -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;
};

View File

@@ -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>
);
};

View File

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

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

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

View File

@@ -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 };
};

View File

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