mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-30 13:03:53 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef26d7129a | ||
|
|
602a61b08d | ||
|
|
d8222d83f0 | ||
|
|
7da5512d45 | ||
|
|
8bf9ae7824 | ||
|
|
f57777e417 | ||
|
|
b3cc3d857a | ||
|
|
bf442d9e70 | ||
|
|
1a556d05ba | ||
|
|
dab301e6d3 | ||
|
|
8ab4b4c788 | ||
|
|
8a5f96a847 | ||
|
|
149fa57075 | ||
|
|
affe184ccd | ||
|
|
1e5e73c4ae | ||
|
|
c76316da03 | ||
|
|
de6205f860 | ||
|
|
f994255091 | ||
|
|
6d4981a3db | ||
|
|
06fef2296f | ||
|
|
999a702291 | ||
|
|
020b9bb2c2 | ||
|
|
7713caab51 | ||
|
|
97a777d729 | ||
|
|
8241d1f08c | ||
|
|
2ac85bbfff | ||
|
|
3f68ae2235 | ||
|
|
0f7b6f75df | ||
|
|
b048e8f5ca | ||
|
|
9783dc45ff | ||
|
|
badbefbade | ||
|
|
b6a265cfad | ||
|
|
9b5ea2f84b | ||
|
|
d8acfa5c05 | ||
|
|
2a5b6924eb | ||
|
|
3b9aee1eb9 |
97
CHANGELOG.md
97
CHANGELOG.md
@@ -2,6 +2,103 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.59.4](https://github.com/wanderer-industries/wanderer/compare/v1.59.3...v1.59.4) (2025-04-10)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.59.3](https://github.com/wanderer-industries/wanderer/compare/v1.59.2...v1.59.3) (2025-04-10)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.59.2](https://github.com/wanderer-industries/wanderer/compare/v1.59.1...v1.59.2) (2025-04-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: fixed connection validation
|
||||
|
||||
## [v1.59.1](https://github.com/wanderer-industries/wanderer/compare/v1.59.0...v1.59.1) (2025-03-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* doc: improve bot setup instructions (#309)
|
||||
|
||||
## [v1.59.0](https://github.com/wanderer-industries/wanderer/compare/v1.58.0...v1.59.0) (2025-03-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: added handling cases when wrong connections created
|
||||
|
||||
## [v1.58.0](https://github.com/wanderer-industries/wanderer/compare/v1.57.1...v1.58.0) (2025-03-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Show online state on map characters page
|
||||
|
||||
* api: update character activity and api to allow date range (#299)
|
||||
|
||||
* api: update character activity and api to allow date range
|
||||
|
||||
## [v1.57.1](https://github.com/wanderer-industries/wanderer/compare/v1.57.0...v1.57.1) (2025-03-20)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.57.0](https://github.com/wanderer-industries/wanderer/compare/v1.56.6...v1.57.0) (2025-03-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* doc: update bot news (#294)
|
||||
|
||||
## [v1.56.6](https://github.com/wanderer-industries/wanderer/compare/v1.56.5...v1.56.6) (2025-03-19)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.56.5](https://github.com/wanderer-industries/wanderer/compare/v1.56.4...v1.56.5) (2025-03-19)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.56.4](https://github.com/wanderer-industries/wanderer/compare/v1.56.3...v1.56.4) (2025-03-19)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.56.3](https://github.com/wanderer-industries/wanderer/compare/v1.56.2...v1.56.3) (2025-03-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* cloak key error behavior (#288)
|
||||
|
||||
## [v1.56.2](https://github.com/wanderer-industries/wanderer/compare/v1.56.1...v1.56.2) (2025-03-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* show signature tooltip on top
|
||||
|
||||
## [v1.56.1](https://github.com/wanderer-industries/wanderer/compare/v1.56.0...v1.56.1) (2025-03-18)
|
||||
|
||||
|
||||
|
||||
7
Makefile
7
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: deploy install cleanup start yarn migrate format test coverage versions
|
||||
.PHONY: deploy install cleanup start yarn migrate format test coverage versions standalone-tests
|
||||
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
SHELL := /bin/bash
|
||||
@@ -35,6 +35,11 @@ test t:
|
||||
coverage cover co:
|
||||
mix test --cover
|
||||
|
||||
unit-tests ut:
|
||||
@echo "Running unit tests..."
|
||||
@find test/unit -name "*.exs" -exec elixir {} \;
|
||||
@echo "All unit tests completed."
|
||||
|
||||
versions v:
|
||||
@echo "Tool Versions"
|
||||
@cat .tool-versions
|
||||
|
||||
@@ -112,19 +112,19 @@ body > div:first-of-type {
|
||||
}
|
||||
|
||||
.wd-characters-icons {
|
||||
display: flex;
|
||||
transition:
|
||||
border-color 250ms,
|
||||
opacity 250ms;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: #5a5a5a;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
/*display: flex;*/
|
||||
/*transition:*/
|
||||
/* border-color 250ms,*/
|
||||
/* opacity 250ms;*/
|
||||
/*width: 35px;*/
|
||||
/*height: 35px;*/
|
||||
/*border-radius: 50%;*/
|
||||
/*border-width: 2px;*/
|
||||
/*border-style: solid;*/
|
||||
/*border-color: #5a5a5a;*/
|
||||
/*background-color: rgba(0, 0, 0, 0);*/
|
||||
/*cursor: pointer;*/
|
||||
/*opacity: 0.6;*/
|
||||
}
|
||||
|
||||
.wd-bg-default {
|
||||
|
||||
@@ -143,3 +143,40 @@
|
||||
background: #966d3d;
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable-wrapper {
|
||||
height: 100%;
|
||||
& {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.5) transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 5px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.p-datatable .p-datatable-tbody > tr.p-highlight {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/* Основной класс диалога */
|
||||
body .p-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
//visibility: hidden;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 10px 0 rgba(0,0,0,0.2);
|
||||
@@ -29,12 +26,10 @@ body .p-dialog {
|
||||
}
|
||||
}
|
||||
|
||||
/* Стиль видимого диалога */
|
||||
.p-dialog-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
.p-dialog-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -53,31 +48,27 @@ body .p-dialog {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
/* Заголовок диалога */
|
||||
.p-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: #f4f4f4;
|
||||
//border-bottom: 1px solid #ddd;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Содержимое диалога */
|
||||
.p-dialog-content {
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Подвал диалога */
|
||||
.p-dialog-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #ddd;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
|
||||
/* Кнопка закрытия диалога */
|
||||
.p-dialog-header-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -93,3 +84,12 @@ body .p-dialog {
|
||||
.p-dialog-header-close .pi {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.p-dialog {
|
||||
.p-dialog-title {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.p-dialog-header-icons {
|
||||
align-self: initial !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.vertical-tabs-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
.p-tabview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.p-tabview-panels {
|
||||
padding: 6px 1rem;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav-container {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav {
|
||||
flex-direction: column;
|
||||
width: 150px;
|
||||
min-height: 100%;
|
||||
border: none;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
border-right: 4px solid var(--surface-hover);
|
||||
background-color: var(--surface-card);
|
||||
|
||||
transition: background-color 200ms, border-right-color 200ms;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--surface-100);
|
||||
}
|
||||
|
||||
.p-tabview-nav-link {
|
||||
transition: color 200ms;
|
||||
|
||||
justify-content: flex-end;
|
||||
padding: 10px;
|
||||
//background-color: var(--surface-card);
|
||||
background-color: initial;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
|
||||
border-radius: initial;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.p-tabview-selected {
|
||||
background-color: var(--surface-50);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
|
||||
.p-tabview-nav-link {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "fix-dialog";
|
||||
@import "fix-popup";
|
||||
@import "fix-tabs";
|
||||
//@import "fix-input";
|
||||
|
||||
//@import "theme";
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.Docked {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 1px;
|
||||
|
||||
background-image: url(/images/citadelLarge.png);
|
||||
left: 2px;
|
||||
top: 22px;
|
||||
transform: rotateZ(0deg);
|
||||
}
|
||||
@@ -4,10 +4,22 @@ 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';
|
||||
|
||||
const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
|
||||
interface CharactersProps {
|
||||
data: CharacterTypeRaw[];
|
||||
}
|
||||
|
||||
export const Characters = ({ data }: CharactersProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const {
|
||||
data: { mainCharacterEveId, followingCharacterEveId },
|
||||
} = useMapRootState();
|
||||
|
||||
const handleSelect = useCallback((character: CharacterTypeRaw) => {
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
@@ -21,21 +33,58 @@ const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
|
||||
className="flex flex-col items-center justify-center"
|
||||
onClick={() => handleSelect(character)}
|
||||
>
|
||||
<div className="tooltip tooltip-bottom" title={character.name}>
|
||||
<a
|
||||
className={clsx('wd-characters-icons wd-bg-default', { ['character-online']: character.online })}
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250',
|
||||
{
|
||||
['border-stone-800/90']: !character.online,
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.name}
|
||||
>
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
|
||||
'text-yellow-500 text-[9px] rounded-[1px] z-10',
|
||||
'pi',
|
||||
PrimeIcons.STAR_FILL,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{followingCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
|
||||
'text-sky-300 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-angle-double-right',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDocked(character.location) && <div className={classes.Docked} />}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full h-full bg-transparent cursor-pointer',
|
||||
'bg-center bg-no-repeat bg-[length:100%]',
|
||||
'transition-opacity',
|
||||
'shadow-[inset_0_1px_6px_1px_#000000]',
|
||||
{
|
||||
['opacity-60']: !character.online,
|
||||
['opacity-100']: character.online,
|
||||
},
|
||||
)}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
|
||||
></a>
|
||||
></div>
|
||||
</div>
|
||||
</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<ul className="flex characters" id="characters" ref={parent}>
|
||||
<ul className="flex gap-1 characters" id="characters" ref={parent}>
|
||||
{items}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export default Characters;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components
|
||||
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';
|
||||
|
||||
export const useContextMenuSystemItems = ({
|
||||
onDeleteSystem,
|
||||
@@ -32,6 +33,7 @@ export const useContextMenuSystemItems = ({
|
||||
|
||||
return useMemo(() => {
|
||||
const system = systemId ? getSystemById(systems, systemId) : undefined;
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
|
||||
if (!system || !systemId) {
|
||||
return [];
|
||||
@@ -44,9 +46,9 @@ export const useContextMenuSystemItems = ({
|
||||
return (
|
||||
<FastSystemActions
|
||||
systemId={systemId}
|
||||
systemName={system.system_static_info.solar_system_name}
|
||||
regionName={system.system_static_info.region_name}
|
||||
isWH={isWormholeSpace(system.system_static_info.system_class)}
|
||||
systemName={systemStaticInfo.solar_system_name}
|
||||
regionName={systemStaticInfo.region_name}
|
||||
isWH={isWormholeSpace(systemStaticInfo.system_class)}
|
||||
showEdit
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
@@ -57,7 +59,7 @@ export const useContextMenuSystemItems = ({
|
||||
getTags(),
|
||||
getStatus(),
|
||||
...getLabels(),
|
||||
...getWaypointMenu(systemId, system.system_static_info.system_class),
|
||||
...getWaypointMenu(systemId, systemStaticInfo.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useCallback } from 'react';
|
||||
import { isPossibleSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { SOLAR_SYSTEM_CLASS_IDS } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
const imperialSpace = [SOLAR_SYSTEM_CLASS_IDS.hs, SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
|
||||
const criminalSpace = [SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
|
||||
@@ -47,7 +47,7 @@ export const useJumpPlannerMenu = (
|
||||
return [];
|
||||
}
|
||||
|
||||
const origin = getSystemById(systems, systemIdFrom)?.system_static_info;
|
||||
const origin = getSystemStaticInfo(systemIdFrom);
|
||||
|
||||
if (!origin) {
|
||||
return [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { getSystemStaticInfo } from '../../mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
interface UseSystemInfoProps {
|
||||
systemId: string;
|
||||
@@ -12,10 +12,8 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
|
||||
data: { systems, connections },
|
||||
} = useMapRootState();
|
||||
|
||||
const { systems: systemStatics } = useLoadSystemStatic({ systems: [systemId] });
|
||||
|
||||
return useMemo(() => {
|
||||
const staticInfo = systemStatics.get(parseInt(systemId));
|
||||
const staticInfo = getSystemStaticInfo(parseInt(systemId));
|
||||
const dynamicInfo = getSystemById(systems, systemId);
|
||||
|
||||
if (!staticInfo || !dynamicInfo) {
|
||||
@@ -29,5 +27,5 @@ export const useSystemInfo = ({ systemId }: UseSystemInfoProps) => {
|
||||
.filter(x => x !== systemId);
|
||||
|
||||
return { dynamicInfo, staticInfo, leadsTo };
|
||||
}, [systemStatics, systemId, systems, connections]);
|
||||
}, [systemId, systems, connections]);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/SystemKillsContent/SystemKillsContent';
|
||||
import { useMemo } from 'react';
|
||||
import { useKillsCounter } from '../../hooks/useKillsCounter';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
|
||||
import {
|
||||
KILLS_ROW_HEIGHT,
|
||||
SystemKillsList,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/SystemKillsList';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
|
||||
const ITEM_HEIGHT = 35;
|
||||
const MIN_TOOLTIP_HEIGHT = 40;
|
||||
|
||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
type KillsBookmarkTooltipProps = {
|
||||
killsCount: number;
|
||||
killsActivityType: string | null;
|
||||
@@ -18,7 +19,13 @@ type KillsBookmarkTooltipProps = {
|
||||
} & WithChildren &
|
||||
WithClassName;
|
||||
|
||||
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
|
||||
export const KillsCounter = ({
|
||||
killsCount,
|
||||
systemId,
|
||||
className,
|
||||
children,
|
||||
size = TooltipSize.xs,
|
||||
}: KillsBookmarkTooltipProps) => {
|
||||
const {
|
||||
isLoading,
|
||||
kills: detailedKills,
|
||||
@@ -37,28 +44,24 @@ export const KillsCounter = ({ killsCount, systemId, className, children, size =
|
||||
}
|
||||
|
||||
// Calculate height based on number of kills, but ensure a minimum height
|
||||
const killsNeededHeight = limitedKills.length * ITEM_HEIGHT;
|
||||
const killsNeededHeight = limitedKills.length * KILLS_ROW_HEIGHT;
|
||||
// Add a small buffer (10px) to prevent scrollbar from appearing unnecessarily
|
||||
const tooltipHeight = Math.max(MIN_TOOLTIP_HEIGHT, Math.min(killsNeededHeight + 10, 500));
|
||||
|
||||
const tooltipContent = (
|
||||
<div
|
||||
style={{
|
||||
width: '400px',
|
||||
height: `${tooltipHeight}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 h-full">
|
||||
<SystemKillsContent kills={limitedKills} systemNameMap={systemNameMap} onlyOneSystem />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<WdTooltipWrapper content={tooltipContent} className={className} size={size} interactive={true}>
|
||||
<WdTooltipWrapper
|
||||
content={
|
||||
<div className="overflow-hidden flex w-[450px] flex-col" style={{ height: `${tooltipHeight}px` }}>
|
||||
<div className="flex-1 h-full">
|
||||
<SystemKillsList kills={limitedKills} onlyOneSystem />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className={className}
|
||||
tooltipClassName="!px-0"
|
||||
size={size}
|
||||
interactive={true}
|
||||
>
|
||||
{children}
|
||||
</WdTooltipWrapper>
|
||||
);
|
||||
|
||||
@@ -355,3 +355,15 @@ $tooltip-bg: #202020;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ShatteredIcon {
|
||||
position: relative;
|
||||
//top: -1px;
|
||||
left: -1px;
|
||||
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
background-image: url(/images/chart-network-svgrepo-com.svg)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useLocalCounter, useSolarSystemNode, useNodeKillsCount } from '../../hooks';
|
||||
import { useLocalCounter, useNodeKillsCount, useSolarSystemNode } from '../../hooks';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
@@ -14,6 +14,8 @@ import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/Worm
|
||||
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';
|
||||
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
@@ -31,8 +33,10 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
)}
|
||||
|
||||
{nodeVars.isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
|
||||
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
|
||||
<span className={clsx('block w-[10px] h-[10px]', classes.ShatteredIcon)} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -40,7 +44,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
<KillsCounter
|
||||
killsCount={localKillsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size="lg"
|
||||
size={TooltipSize.lg}
|
||||
killsActivityType={nodeVars.killsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||
>
|
||||
|
||||
@@ -14,6 +14,8 @@ import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/Worm
|
||||
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';
|
||||
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
@@ -31,8 +33,10 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
)}
|
||||
|
||||
{nodeVars.isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered, '!pr-[2px]')}>
|
||||
<WdTooltipWrapper content="Shattered" position={TooltipPosition.top}>
|
||||
<span className={clsx('block w-[10px] h-[10px]', classes.ShatteredIcon)} />
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -40,7 +44,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
<KillsCounter
|
||||
killsCount={localKillsCount}
|
||||
systemId={nodeVars.solarSystemId}
|
||||
size="lg"
|
||||
size={TooltipSize.lg}
|
||||
killsActivityType={nodeVars.killsActivityType}
|
||||
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||
>
|
||||
|
||||
@@ -2,10 +2,13 @@ import { Node, useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandAddSystems } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { convertSystem2Node } from '../../helpers';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
export const useMapAddSystems = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
|
||||
|
||||
const ref = useRef({ rf });
|
||||
ref.current = { rf };
|
||||
|
||||
@@ -13,7 +16,10 @@ export const useMapAddSystems = () => {
|
||||
const { rf } = ref.current;
|
||||
const nodes = rf.getNodes();
|
||||
|
||||
const prepared: Node[] = systems.filter(x => !nodes.some(y => x.id === y.id)).map(convertSystem2Node);
|
||||
const newSystems = systems.filter(x => !nodes.some(y => x.id === y.id));
|
||||
newSystems.forEach(x => addSystemStatic(x.system_static_info));
|
||||
|
||||
const prepared: Node[] = newSystems.map(convertSystem2Node);
|
||||
rf.addNodes(prepared);
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useMapInit = () => {
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
system_signatures,
|
||||
kills,
|
||||
connections,
|
||||
wormholes,
|
||||
@@ -51,6 +52,10 @@ export const useMapInit = () => {
|
||||
updateData.systems = systems;
|
||||
}
|
||||
|
||||
if (system_signatures) {
|
||||
updateData.systemSignatures = system_signatures;
|
||||
}
|
||||
|
||||
if (kills) {
|
||||
updateData.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSystemKills } from '../../mapInterface/widgets/SystemKills/hooks/useSystemKills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useSystemKills } from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/hooks/useSystemKills.ts';
|
||||
|
||||
interface UseKillsCounterProps {
|
||||
realSystemId: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/ty
|
||||
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
|
||||
import { useSystemName } from './useSystemName';
|
||||
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
function getActivityType(count: number): string {
|
||||
if (count <= 5) return 'activityNormal';
|
||||
@@ -43,8 +44,7 @@ export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
|
||||
const { id, data, selected } = props;
|
||||
const {
|
||||
system_static_info,
|
||||
system_signatures,
|
||||
id: solar_system_id,
|
||||
locked,
|
||||
name,
|
||||
tag,
|
||||
@@ -54,23 +54,24 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
linked_sig_eve_id: linkedSigEveId = '',
|
||||
} = data;
|
||||
|
||||
const {
|
||||
interfaceSettings,
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
} = useMapRootState();
|
||||
|
||||
const {
|
||||
system_class,
|
||||
security,
|
||||
class_title,
|
||||
solar_system_id,
|
||||
statics,
|
||||
effect_name,
|
||||
region_name,
|
||||
region_id,
|
||||
is_shattered,
|
||||
solar_system_name,
|
||||
} = system_static_info;
|
||||
|
||||
const {
|
||||
interfaceSettings,
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
} = useMapRootState();
|
||||
} = useMemo(() => {
|
||||
return getSystemStaticInfo(parseInt(solar_system_id))!;
|
||||
}, [solar_system_id]);
|
||||
|
||||
const { isShowUnsplashedSignatures } = interfaceSettings;
|
||||
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
|
||||
@@ -95,13 +96,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const systemSigs = useMemo(
|
||||
() => mapSystemSignatures[solar_system_id] || system_signatures,
|
||||
[system_signatures, solar_system_id, mapSystemSignatures],
|
||||
);
|
||||
const systemSigs = useMemo(() => mapSystemSignatures[solar_system_id] || [], [solar_system_id, mapSystemSignatures]);
|
||||
|
||||
const charactersInSystem = useMemo(() => {
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
|
||||
return characters.filter(c => c.location?.solar_system_id === parseInt(solar_system_id) && c.online);
|
||||
}, [characters, solar_system_id]);
|
||||
|
||||
const isWormhole = isWormholeSpace(system_class);
|
||||
@@ -121,7 +119,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarS
|
||||
isShowLinkedSigId,
|
||||
});
|
||||
|
||||
const killsCount = useMemo(() => kills[solar_system_id] ?? null, [kills, solar_system_id]);
|
||||
const killsCount = useMemo(() => kills[parseInt(solar_system_id)] ?? null, [kills, solar_system_id]);
|
||||
const killsActivityType = killsCount ? getActivityType(killsCount) : null;
|
||||
|
||||
const hasUserCharacters = useMemo(
|
||||
|
||||
@@ -542,48 +542,32 @@
|
||||
background-color: #d10600;
|
||||
}
|
||||
|
||||
.react-flow {
|
||||
color: var(--text-color);
|
||||
|
||||
&__pane {
|
||||
cursor: auto;
|
||||
}
|
||||
.react-flow__minimap-node {
|
||||
fill: #ffb03a;
|
||||
}
|
||||
|
||||
&__minimap {
|
||||
background-color: rgba(66, 66, 66, 1);
|
||||
opacity: 0.7;
|
||||
border: 1px solid #2f2f2f;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.react-flow__minimap {
|
||||
border: 1px solid #282828;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(47 37 37) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__minimap-mask {
|
||||
fill: rgba(28, 28, 28, 0.75);
|
||||
}
|
||||
.react-flow__minimap-mask {
|
||||
stroke-width: 2px;
|
||||
fill: rgba(0, 0, 0, 0.5);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
filter: brightness(1.5);
|
||||
}
|
||||
|
||||
&__minimap-node {
|
||||
fill: #ffb03a;
|
||||
}
|
||||
.react-flow__minimap-mask {
|
||||
stroke-width: 2px;
|
||||
fill: rgb(0 0 0 / 50%) !important;
|
||||
mix-blend-mode: inherit;
|
||||
opacity: 1;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
.context-menu-active {
|
||||
background-color: rgba(131, 131, 131, 0.33);
|
||||
}
|
||||
|
||||
.p-dialog {
|
||||
.p-dialog-header {
|
||||
height: 40px;
|
||||
padding: 1rem;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
.p-dialog-title {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
.p-dialog-header-icons {
|
||||
align-self: initial !important;
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export const Comments = ({}: CommentsProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 mt-1 whitespace-nowrap overflow-auto text-ellipsis custom-scrollbar">
|
||||
<div className="flex flex-col gap-1 whitespace-nowrap overflow-auto text-ellipsis custom-scrollbar">
|
||||
{commentsList.map(({ id, text, updated_at, characterEveId }) => (
|
||||
<MarkdownComment key={id} text={text} time={updated_at} characterEveId={characterEveId} id={id} />
|
||||
))}
|
||||
|
||||
@@ -16,7 +16,6 @@ const stopEventPropagationPlugin = ViewPlugin.fromClass(
|
||||
|
||||
// @ts-ignore
|
||||
this.pasteHandler = (event: Event) => {
|
||||
console.log('Paste done in editor, stopping global listeners.');
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
interface SystemSettingsDialog {
|
||||
systemId: string;
|
||||
@@ -26,6 +27,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
|
||||
|
||||
const system = getSystemById(systems, systemId);
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId);
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [label, setLabel] = useState('');
|
||||
@@ -33,11 +35,11 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
const [description, setDescription] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system });
|
||||
ref.current = { name, description, label, temporaryName, outCommand, systemId, system };
|
||||
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system, systemStaticInfo });
|
||||
ref.current = { name, description, label, temporaryName, outCommand, systemId, system, systemStaticInfo };
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const { name, description, label, temporaryName, outCommand, systemId, system } = ref.current;
|
||||
const { name, description, label, temporaryName, outCommand, systemId, system, systemStaticInfo } = ref.current;
|
||||
|
||||
const outLabel = new LabelsManager(system?.labels ?? '');
|
||||
outLabel.updateCustomLabel(label);
|
||||
@@ -62,7 +64,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
type: OutCommand.updateSystemName,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
value: name.trim() || system?.system_static_info.solar_system_name,
|
||||
value: name.trim() || systemStaticInfo?.solar_system_name,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,11 +80,11 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
}, [setVisible]);
|
||||
|
||||
const handleResetSystemName = useCallback(() => {
|
||||
const { system } = ref.current;
|
||||
if (!system) {
|
||||
const { systemStaticInfo } = ref.current;
|
||||
if (!systemStaticInfo) {
|
||||
return;
|
||||
}
|
||||
setName(system.system_static_info.solar_system_name);
|
||||
setName(systemStaticInfo.solar_system_name);
|
||||
}, []);
|
||||
|
||||
const onShow = useCallback(() => {
|
||||
@@ -130,7 +132,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
<label htmlFor="username">Custom name</label>
|
||||
|
||||
<IconField>
|
||||
{name !== system?.system_static_info.solar_system_name && (
|
||||
{name !== systemStaticInfo?.solar_system_name && (
|
||||
<WdImgButton
|
||||
className="pi pi-undo"
|
||||
textSize={WdImageSize.large}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.root {
|
||||
padding-bottom: 5px;
|
||||
|
||||
}
|
||||
|
||||
.Header {
|
||||
|
||||
@@ -2,14 +2,15 @@ import React from 'react';
|
||||
|
||||
import classes from './Widget.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
export interface WidgetProps {
|
||||
export type WidgetProps = {
|
||||
label: React.ReactNode | string;
|
||||
windowId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
contentClassName?: string;
|
||||
} & WithChildren;
|
||||
|
||||
export const Widget = ({ label, children, windowId }: WidgetProps) => {
|
||||
export const Widget = ({ label, children, windowId, contentClassName }: WidgetProps) => {
|
||||
return (
|
||||
<div
|
||||
data-window-id={windowId}
|
||||
@@ -34,7 +35,7 @@ export const Widget = ({ label, children, windowId }: WidgetProps) => {
|
||||
{label}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(classes.Content, 'overflow-auto', 'bg-opacity-5 custom-scrollbar')}
|
||||
className={clsx(classes.Content, 'overflow-auto', 'bg-opacity-5 custom-scrollbar', contentClassName)}
|
||||
style={{ flexGrow: 1 }}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SystemInfo,
|
||||
SystemSignatures,
|
||||
SystemStructures,
|
||||
SystemKills,
|
||||
WSystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { CommentsWidget } from '@/hooks/Mapper/components/mapInterface/widgets/CommentsWidget';
|
||||
|
||||
@@ -70,7 +70,7 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
position: { x: 270, y: 730 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemKills />,
|
||||
content: () => <WSystemKills />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.comments,
|
||||
|
||||
@@ -44,6 +44,7 @@ export const CommentsWidget = () => {
|
||||
|
||||
return (
|
||||
<Widget
|
||||
contentClassName="my-1"
|
||||
label={
|
||||
<div ref={containerRef} className="flex justify-between items-center gap-1 text-xs w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -5,20 +5,20 @@ import { SystemInfoContent } from './SystemInfoContent';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
export const SystemInfo = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems, systems },
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const sys = getSystemById(systems, systemId)!;
|
||||
const { solar_system_name: solarSystemName } = sys?.system_static_info || {};
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
const { solar_system_name: solarSystemName } = systemStaticInfo || {};
|
||||
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
|
||||
import { useMemo } from 'react';
|
||||
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
interface SystemInfoContentProps {
|
||||
systemId: string;
|
||||
@@ -14,9 +15,9 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
|
||||
} = useMapRootState();
|
||||
|
||||
const sys = getSystemById(systems, systemId)! || {};
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
const { description } = sys;
|
||||
const { system_class, region_name, constellation_name, statics, effect_name, effect_power } =
|
||||
sys.system_static_info || {};
|
||||
const { system_class, region_name, constellation_name, statics, effect_name, effect_power } = systemStaticInfo || {};
|
||||
const isWH = isWormholeSpace(system_class);
|
||||
const sortedStatics = useMemo(() => sortWHClasses(wormholesData, statics), [wormholesData, statics]);
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SystemKillsContent } from './SystemKillsContent/SystemKillsContent';
|
||||
import { KillsHeader } from './components/SystemKillsHeader';
|
||||
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
|
||||
import { useSystemKills } from './hooks/useSystemKills';
|
||||
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
|
||||
export const SystemKills: React.FC = React.memo(() => {
|
||||
const {
|
||||
data: { selectedSystems, systems, isSubscriptionActive },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
|
||||
|
||||
const systemNameMap = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
systems.forEach(sys => {
|
||||
map[sys.id] = sys.temporary_name || sys.name || '???';
|
||||
});
|
||||
return map;
|
||||
}, [systems]);
|
||||
|
||||
const systemBySolarSystemId = useMemo(() => {
|
||||
const map: Record<number, SolarSystemRawType> = {};
|
||||
systems.forEach(sys => {
|
||||
if (sys.system_static_info?.solar_system_id != null) {
|
||||
map[sys.system_static_info.solar_system_id] = sys;
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [systems]);
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const visible = settings.showAll;
|
||||
|
||||
const { kills, isLoading, error } = useSystemKills({
|
||||
systemId,
|
||||
outCommand,
|
||||
showAllVisible: visible,
|
||||
sinceHours: settings.timeRange,
|
||||
});
|
||||
|
||||
const isNothingSelected = !systemId && !visible;
|
||||
const showLoading = isLoading && kills.length === 0;
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settings.whOnly || !visible) return kills;
|
||||
return kills.filter(kill => {
|
||||
const system = systemBySolarSystemId[kill.solar_system_id];
|
||||
if (!system) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
return false;
|
||||
}
|
||||
return isWormholeSpace(system.system_static_info.system_class);
|
||||
});
|
||||
}, [kills, settings.whOnly, systemBySolarSystemId, visible]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />}>
|
||||
{!isSubscriptionActive ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</span>
|
||||
</div>
|
||||
) : isNothingSelected ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No system selected (or toggle "Show all systems")
|
||||
</span>
|
||||
</div>
|
||||
) : showLoading ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">Loading Kills...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-red-400 text-sm">{error}</span>
|
||||
</div>
|
||||
) : !filteredKills || filteredKills.length === 0 ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">No kills found</span>
|
||||
</div>
|
||||
) : (
|
||||
<SystemKillsContent
|
||||
kills={filteredKills}
|
||||
systemNameMap={systemNameMap}
|
||||
onlyOneSystem={!visible}
|
||||
timeRange={settings.timeRange}
|
||||
/>
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
{settingsDialogVisible && <KillsSettingsDialog visible setVisible={setSettingsDialogVisible} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SystemKills.displayName = 'SystemKills';
|
||||
@@ -1,35 +0,0 @@
|
||||
// Custom scrollbar styling is now handled by the global custom-scrollbar class
|
||||
.scrollerContent {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
// VirtualScroller specific styles that can't be handled with Tailwind
|
||||
.VirtualScroller {
|
||||
overflow: hidden !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: 100% !important;
|
||||
|
||||
// Target this specific VirtualScroller instance
|
||||
&:global(.p-virtualscroller) {
|
||||
height: 100% !important;
|
||||
|
||||
:global(.p-virtualscroller-content) {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for PrimeReact VirtualScroller - these need to be global
|
||||
:global {
|
||||
.p-virtualscroller {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.p-virtualscroller-content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import { KillRow } from './SystemKillsRow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export function KillItemTemplate(
|
||||
systemNameMap: Record<string, string>,
|
||||
onlyOneSystem: boolean,
|
||||
kill: DetailedKill,
|
||||
options: VirtualScrollerTemplateOptions,
|
||||
) {
|
||||
const systemIdStr = String(kill.solar_system_id);
|
||||
const systemName = systemNameMap[systemIdStr] || `System ${systemIdStr}`;
|
||||
|
||||
return (
|
||||
<div style={{ height: `${options.props.itemSize}px` }} className={clsx({ 'bg-gray-900': options.odd })}>
|
||||
<KillRow killDetails={kill} systemName={systemName} onlyOneSystem={onlyOneSystem} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { KillRowDetail } from './KillRowDetail.tsx';
|
||||
|
||||
export interface KillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
onlyOneSystem?: boolean;
|
||||
}
|
||||
|
||||
const KillRowComponent: React.FC<KillRowProps> = ({ killDetails, systemName, onlyOneSystem = false }) => {
|
||||
return <KillRowDetail killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
|
||||
};
|
||||
|
||||
export const KillRow = React.memo(KillRowComponent);
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SystemKills';
|
||||
@@ -1,79 +0,0 @@
|
||||
.verticalTabsContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
:global {
|
||||
.p-tabview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.p-tabview-panels {
|
||||
padding: 6px 1rem !important;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.p-tabview-nav-container {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav {
|
||||
flex-direction: column;
|
||||
width: 150px;
|
||||
min-height: 100%;
|
||||
border: none;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
border-right: 4px solid var(--surface-hover);
|
||||
background-color: var(--surface-card);
|
||||
|
||||
transition:
|
||||
background-color 200ms,
|
||||
border-right-color 200ms;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--surface-100);
|
||||
}
|
||||
|
||||
.p-tabview-nav-link {
|
||||
transition: color 200ms;
|
||||
|
||||
justify-content: flex-end;
|
||||
padding: 10px;
|
||||
//background-color: var(--surface-card);
|
||||
background-color: initial;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
|
||||
border-radius: initial;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.p-tabview-selected {
|
||||
background-color: var(--surface-50);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
|
||||
.p-tabview-nav-link {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from 'primereact/button';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import styles from './SystemSignatureSettingsDialog.module.scss';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import {
|
||||
@@ -72,26 +71,24 @@ export const SystemSignatureSettingsDialog = ({
|
||||
<Dialog header="System Signatures Settings" visible={true} onHide={onCancel} className="w-full max-w-lg h-[500px]">
|
||||
<div className="flex flex-col gap-3 justify-between h-full">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className={styles.verticalTabsContainer}>
|
||||
<TabView
|
||||
activeIndex={activeIndex}
|
||||
onTabChange={e => setActiveIndex(e.index)}
|
||||
className={styles.verticalTabView}
|
||||
>
|
||||
<TabPanel header="Filters" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
{SIGNATURE_SETTINGS.filterFlags.map(renderSetting)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
{SIGNATURE_SETTINGS.uiFlags.map(renderSetting)}
|
||||
<div className="my-2 border-t border-stone-700/50"></div>
|
||||
{SIGNATURE_SETTINGS.uiOther.map(renderSetting)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</div>
|
||||
<TabView
|
||||
activeIndex={activeIndex}
|
||||
onTabChange={e => setActiveIndex(e.index)}
|
||||
className="vertical-tabs-container"
|
||||
>
|
||||
<TabPanel header="Filters">
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
{SIGNATURE_SETTINGS.filterFlags.map(renderSetting)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="User Interface">
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
{SIGNATURE_SETTINGS.uiFlags.map(renderSetting)}
|
||||
<div className="my-2 border-t border-stone-700/50"></div>
|
||||
{SIGNATURE_SETTINGS.uiOther.map(renderSetting)}
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { Column } from 'primereact/column';
|
||||
import {
|
||||
DataTable,
|
||||
DataTableRowClickEvent,
|
||||
@@ -6,13 +7,9 @@ import {
|
||||
DataTableStateEvent,
|
||||
SortOrder,
|
||||
} from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
|
||||
import { WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
|
||||
import {
|
||||
COMPACT_MAX_WIDTH,
|
||||
@@ -24,6 +21,9 @@ import {
|
||||
SIGNATURE_WINDOW_ID,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
|
||||
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
|
||||
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
import {
|
||||
renderAddedTimeLeft,
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
renderInfoColumn,
|
||||
renderUpdatedTimeLeft,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
|
||||
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
|
||||
|
||||
@@ -348,6 +348,7 @@ export const SystemSignaturesContent = ({
|
||||
<WdTooltip
|
||||
className="bg-stone-900/95 text-slate-50"
|
||||
ref={tooltipRef}
|
||||
position={TooltipPosition.top}
|
||||
content={
|
||||
hoveredSignature ? (
|
||||
<SignatureView signature={hoveredSignature} showCharacterPortrait={showCharacterPortrait} />
|
||||
|
||||
@@ -32,7 +32,12 @@ export function parseFormatOneLine(line: string): StructureItem | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawTypeName != STRUCTURE_TYPE_MAP[rawTypeId]) {
|
||||
// in some localizations (like russian) there is an option called "mark names with *"
|
||||
// The example output will be "35826 Itamo - Research & Production Azbel* 609 м"
|
||||
// so, let's fix this
|
||||
const localizationFixedName = rawTypeName.replace("*", "");
|
||||
|
||||
if (localizationFixedName != STRUCTURE_TYPE_MAP[rawTypeId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { VirtualScroller } from 'primereact/virtualscroller';
|
||||
import { useSystemKillsItemTemplate } from '../hooks/useSystemKillsItemTemplate';
|
||||
import classes from './SystemKillsContent.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
export const ITEM_HEIGHT = 35;
|
||||
export const KILLS_ROW_HEIGHT = 40;
|
||||
|
||||
export interface SystemKillsContentProps {
|
||||
export type SystemKillsContentProps = {
|
||||
kills: DetailedKill[];
|
||||
systemNameMap: Record<string, string>;
|
||||
onlyOneSystem?: boolean;
|
||||
timeRange?: number;
|
||||
limit?: number;
|
||||
}
|
||||
} & WithClassName;
|
||||
|
||||
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
|
||||
export const SystemKillsList = ({
|
||||
kills,
|
||||
systemNameMap,
|
||||
onlyOneSystem = false,
|
||||
timeRange = 4,
|
||||
limit,
|
||||
}) => {
|
||||
className,
|
||||
}: SystemKillsContentProps) => {
|
||||
const processedKills = useMemo(() => {
|
||||
if (!kills || kills.length === 0) return [];
|
||||
|
||||
@@ -47,30 +47,17 @@ export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
|
||||
return filteredKills;
|
||||
}, [kills, timeRange, limit]);
|
||||
|
||||
const itemTemplate = useSystemKillsItemTemplate(systemNameMap, onlyOneSystem);
|
||||
|
||||
// Define style for the VirtualScroller
|
||||
const virtualScrollerStyle: React.CSSProperties = {
|
||||
boxSizing: 'border-box',
|
||||
height: '100%', // Use 100% height to fill the container
|
||||
};
|
||||
const itemTemplate = useSystemKillsItemTemplate(onlyOneSystem);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden" data-testid="system-kills-content">
|
||||
<VirtualScroller
|
||||
items={processedKills}
|
||||
itemSize={ITEM_HEIGHT}
|
||||
itemTemplate={itemTemplate}
|
||||
className={`w-full h-full flex-1 select-none ${classes.VirtualScroller}`}
|
||||
style={virtualScrollerStyle}
|
||||
pt={{
|
||||
content: {
|
||||
className: `custom-scrollbar ${classes.scrollerContent}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<VirtualScroller
|
||||
items={processedKills}
|
||||
itemSize={KILLS_ROW_HEIGHT}
|
||||
itemTemplate={itemTemplate}
|
||||
className={clsx(
|
||||
'w-full flex-1 select-none !h-full overflow-x-hidden overflow-y-auto custom-scrollbar',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemKillsContent;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SystemKillsList';
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SystemKillsList } from './SystemKillsList';
|
||||
import { KillsHeader } from './components/SystemKillsHeader';
|
||||
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
|
||||
import { useSystemKills } from './hooks/useSystemKills';
|
||||
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
const SystemKillsContent = () => {
|
||||
const {
|
||||
data: { selectedSystems, isSubscriptionActive },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
|
||||
const systemStaticInfo = getSystemStaticInfo(systemId)!;
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const visible = settings.showAll;
|
||||
|
||||
const { kills, isLoading, error } = useSystemKills({
|
||||
systemId,
|
||||
outCommand,
|
||||
showAllVisible: visible,
|
||||
sinceHours: settings.timeRange,
|
||||
});
|
||||
|
||||
const isNothingSelected = !systemId && !visible;
|
||||
const showLoading = isLoading && kills.length === 0;
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settings.whOnly || !visible) return kills;
|
||||
return kills.filter(kill => {
|
||||
if (!systemStaticInfo) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
return false;
|
||||
}
|
||||
return isWormholeSpace(systemStaticInfo.system_class);
|
||||
});
|
||||
}, [kills, settings.whOnly, systemStaticInfo, visible]);
|
||||
|
||||
if (!isSubscriptionActive) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNothingSelected) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">
|
||||
No system selected (or toggle "Show all systems")
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">Loading Kills...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-red-400 text-sm">{error}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!filteredKills || filteredKills.length === 0) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<span className="select-none text-center text-stone-400/80 text-sm">No kills found</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SystemKillsList kills={filteredKills} onlyOneSystem={!visible} timeRange={settings.timeRange} />;
|
||||
};
|
||||
|
||||
export const WSystemKills = () => {
|
||||
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
|
||||
const handleOpenSettings = useCallback(() => setSettingsDialogVisible(true), []);
|
||||
|
||||
return (
|
||||
<Widget label={<KillsHeader systemId={systemId} onOpenSettings={handleOpenSettings} />}>
|
||||
<SystemKillsContent />
|
||||
{settingsDialogVisible && <KillsSettingsDialog visible setVisible={setSettingsDialogVisible} />}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import { KillRowDetail } from '@/hooks/Mapper/components/mapInterface/widgets/WSystemKills/components/KillRowDetail.tsx';
|
||||
import clsx from 'clsx';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
export const KillItemTemplate = (
|
||||
onlyOneSystem: boolean,
|
||||
kill: DetailedKill,
|
||||
options: VirtualScrollerTemplateOptions,
|
||||
) => {
|
||||
const systemName = getSystemStaticInfo(kill.solar_system_id)?.solar_system_name || `System ${kill.solar_system_id}`;
|
||||
|
||||
return (
|
||||
<div style={{ height: `${options.props.itemSize}px` }}>
|
||||
<KillRowDetail
|
||||
killDetails={kill}
|
||||
systemName={systemName}
|
||||
onlyOneSystem={onlyOneSystem}
|
||||
className={clsx(options.odd && 'bg-stone-800/50')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import {
|
||||
@@ -14,14 +14,15 @@ import {
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import classes from './KillRowDetail.module.scss';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
export interface CompactKillRowProps {
|
||||
export type CompactKillRowProps = {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
} & WithClassName;
|
||||
|
||||
export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, systemName, onlyOneSystem }) => {
|
||||
export const KillRowDetail = ({ killDetails, systemName, onlyOneSystem, className }: CompactKillRowProps) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
// Victim data
|
||||
@@ -80,13 +81,24 @@ export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, syst
|
||||
'Victim',
|
||||
);
|
||||
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id,
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = useMemo(
|
||||
() =>
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id,
|
||||
),
|
||||
[
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerIsNpc,
|
||||
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.
|
||||
@@ -100,6 +112,8 @@ export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, syst
|
||||
className={clsx(
|
||||
'h-10 flex items-center border-b border-stone-800',
|
||||
'text-xs whitespace-nowrap overflow-hidden leading-none',
|
||||
'px-1',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Victim Section */}
|
||||
@@ -142,12 +156,14 @@ export const KillRowDetail: React.FC<CompactKillRowProps> = ({ killDetails, syst
|
||||
{victim_char_name}
|
||||
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
<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]">
|
||||
{victim_ship_name}
|
||||
</span>
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
<span className="text-stone-400">/</span>
|
||||
<span className="text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
|
||||
import { useMapEventListener, MapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
interface UseSystemKillsProps {
|
||||
systemId?: string;
|
||||
@@ -29,10 +28,6 @@ function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceH
|
||||
return Object.values(byId);
|
||||
}
|
||||
|
||||
interface DetailedKillsEvent extends MapEvent<Commands> {
|
||||
payload: Record<string, DetailedKill[]>;
|
||||
}
|
||||
|
||||
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
|
||||
const { data, update } = useMapRootState();
|
||||
const { detailedKills = {}, systems = [] } = data;
|
||||
@@ -41,32 +36,6 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
||||
|
||||
const effectiveSinceHours = sinceHours;
|
||||
|
||||
const updateDetailedKills = useCallback(
|
||||
(newKillsMap: Record<string, DetailedKill[]>) => {
|
||||
update(prev => {
|
||||
const oldKills = prev.detailedKills ?? {};
|
||||
const updated = { ...oldKills };
|
||||
for (const [sid, killsArr] of Object.entries(newKillsMap)) {
|
||||
updated[sid] = killsArr;
|
||||
}
|
||||
return { ...prev, detailedKills: updated };
|
||||
}, true);
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
useMapEventListener((event: MapEvent<Commands>) => {
|
||||
if (event.name === Commands.detailedKillsUpdated) {
|
||||
const detailedEvent = event as DetailedKillsEvent;
|
||||
if (systemId && !Object.keys(detailedEvent.payload).includes(systemId.toString())) {
|
||||
return false;
|
||||
}
|
||||
updateDetailedKills(detailedEvent.payload);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const effectiveSystemIds = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
@@ -4,10 +4,9 @@ import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { KillItemTemplate } from '../components/KillItemTemplate';
|
||||
|
||||
export function useSystemKillsItemTemplate(systemNameMap: Record<string, string>, onlyOneSystem: boolean) {
|
||||
export function useSystemKillsItemTemplate(onlyOneSystem: boolean) {
|
||||
return useCallback(
|
||||
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) =>
|
||||
KillItemTemplate(systemNameMap, onlyOneSystem, kill, options),
|
||||
[systemNameMap, onlyOneSystem],
|
||||
(kill: DetailedKill, options: VirtualScrollerTemplateOptions) => KillItemTemplate(onlyOneSystem, kill, options),
|
||||
[onlyOneSystem],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WSystemKills';
|
||||
@@ -3,4 +3,4 @@ export * from './SystemInfo';
|
||||
export * from './RoutesWidget';
|
||||
export * from './SystemSignatures';
|
||||
export * from './SystemStructures';
|
||||
export * from './SystemKills';
|
||||
export * from './WSystemKills';
|
||||
|
||||
@@ -9,9 +9,10 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
|
||||
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
|
||||
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
|
||||
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
|
||||
import { TrackAndFollow } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/TrackAndFollow';
|
||||
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
|
||||
import { useTrackAndFollowHandlers } from './hooks/useTrackAndFollowHandlers';
|
||||
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
|
||||
export interface MapRootContentProps {}
|
||||
|
||||
@@ -19,18 +20,28 @@ export interface MapRootContentProps {}
|
||||
export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const { interfaceSettings, data } = useMapRootState();
|
||||
const { isShowMenu } = interfaceSettings;
|
||||
const { showCharacterActivity, showTrackAndFollow } = data;
|
||||
const { showCharacterActivity } = data;
|
||||
const { handleHideCharacterActivity } = useCharacterActivityHandlers();
|
||||
const { handleHideTracking } = useTrackAndFollowHandlers();
|
||||
|
||||
const themeClass = `${interfaceSettings.theme ?? 'default'}-theme`;
|
||||
|
||||
const [showOnTheMap, setShowOnTheMap] = useState(false);
|
||||
const [showMapSettings, setShowMapSettings] = useState(false);
|
||||
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
|
||||
|
||||
/* Important Notice - this solution needs for use one instance of MapInterface */
|
||||
const mapInterface = <MapInterface />;
|
||||
|
||||
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
|
||||
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
|
||||
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.showTracking) {
|
||||
setShowTrackingDialog(true);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
useSkipContextMenu();
|
||||
|
||||
@@ -44,13 +55,21 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
{mapInterface}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto">
|
||||
<RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
|
||||
<RightBar
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
|
||||
<Topbar>
|
||||
<MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
|
||||
<MapContextMenu
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
/>
|
||||
</Topbar>
|
||||
{mapInterface}
|
||||
</div>
|
||||
@@ -60,7 +79,9 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
{showCharacterActivity && (
|
||||
<CharacterActivity visible={showCharacterActivity} onHide={handleHideCharacterActivity} />
|
||||
)}
|
||||
{showTrackAndFollow && <TrackAndFollow visible={showTrackAndFollow} onHide={handleHideTracking} />}
|
||||
{showTrackingDialog && (
|
||||
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
|
||||
)}
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,129 +1,17 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import { CharacterCard } from '../../../ui-kit';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
export interface ActivitySummary {
|
||||
character: CharacterTypeRaw;
|
||||
passages: number;
|
||||
connections: number;
|
||||
signatures: number;
|
||||
}
|
||||
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
|
||||
|
||||
interface CharacterActivityProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const getRowClassName = () => ['text-xs', 'leading-tight'];
|
||||
|
||||
const renderCharacterTemplate = (rowData: ActivitySummary) => {
|
||||
return <CharacterCard compact isOwn {...rowData.character} />;
|
||||
};
|
||||
|
||||
const renderValueTemplate = (rowData: ActivitySummary, field: keyof ActivitySummary) => {
|
||||
return <div className="tabular-nums">{rowData[field] as number}</div>;
|
||||
};
|
||||
|
||||
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
|
||||
const { data } = useMapRootState();
|
||||
const { characterActivityData } = data;
|
||||
const [localActivity, setLocalActivity] = useState<ActivitySummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const activity = useMemo(() => {
|
||||
return characterActivityData?.activity || [];
|
||||
}, [characterActivityData]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalActivity(activity);
|
||||
setLoading(characterActivityData?.loading !== false);
|
||||
}, [activity, characterActivityData]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[400px] w-full">
|
||||
<ProgressSpinner className="w-[50px] h-[50px]" strokeWidth="4" />
|
||||
<div className="mt-4 text-text-color-secondary text-sm">Loading character activity data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (localActivity.length === 0) {
|
||||
return (
|
||||
<div className="p-8 text-center text-text-color-secondary italic">No character activity data available</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
value={localActivity}
|
||||
scrollable
|
||||
scrollHeight="400px"
|
||||
resizableColumns
|
||||
columnResizeMode="fit"
|
||||
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
|
||||
// headerStyle={{ minWidth: '75px', height: 'auto', overflow: 'visible' }}
|
||||
// bodyStyle={{ minWidth: '75px' }}
|
||||
// className={classes.characterColumn}
|
||||
// headerClassName={classes.columnHeader}
|
||||
/>
|
||||
|
||||
<Column
|
||||
field="passages"
|
||||
header="Passages"
|
||||
body={rowData => renderValueTemplate(rowData, 'passages')}
|
||||
sortable
|
||||
// headerStyle={{ width: '120px', textAlign: 'center', height: 'auto', overflow: 'visible' }}
|
||||
// bodyStyle={{ width: '120px', textAlign: 'center' }}
|
||||
// className={classes.numericColumn}
|
||||
// headerClassName={classes.columnHeader}
|
||||
/>
|
||||
<Column
|
||||
field="connections"
|
||||
header="Connections"
|
||||
body={rowData => renderValueTemplate(rowData, 'connections')}
|
||||
sortable
|
||||
// headerStyle={{ width: '120px', textAlign: 'center', height: 'auto', overflow: 'visible' }}
|
||||
// bodyStyle={{ width: '120px', textAlign: 'center' }}
|
||||
// className={classes.numericColumn}
|
||||
// headerClassName={classes.columnHeader}
|
||||
/>
|
||||
<Column
|
||||
field="signatures"
|
||||
header="Signatures"
|
||||
body={rowData => renderValueTemplate(rowData, 'signatures')}
|
||||
sortable
|
||||
// headerStyle={{ width: '120px', textAlign: 'center', height: 'auto', overflow: 'visible' }}
|
||||
// bodyStyle={{ width: '120px', textAlign: 'center' }}
|
||||
// className={classes.numericColumn}
|
||||
// headerClassName={classes.columnHeader}
|
||||
/>
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog header="Character Activity" visible={visible} className="max-w-[600px]" onHide={onHide} dismissableMask>
|
||||
<div className="w-full h-[400px] flex flex-col overflow-hidden p-0 m-0">{renderContent()}</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import {
|
||||
getRowClassName,
|
||||
renderCharacterTemplate,
|
||||
renderValueTemplate,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
|
||||
import { Column } from 'primereact/column';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const CharacterActivityContent = () => {
|
||||
const {
|
||||
data: { characterActivityData },
|
||||
} = useMapRootState();
|
||||
|
||||
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
|
||||
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full w-full">
|
||||
<ProgressSpinner className="w-[50px] h-[50px]" strokeWidth="4" />
|
||||
<div className="mt-4 text-text-color-secondary text-sm">Loading character activity data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activity.length === 0) {
|
||||
return <div className="p-8 text-center text-text-color-secondary italic">No character activity data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ActivitySummary } from '@/hooks/Mapper/types';
|
||||
|
||||
export const getRowClassName = () => ['text-xs', 'leading-tight'];
|
||||
|
||||
export const renderCharacterTemplate = (rowData: ActivitySummary) => {
|
||||
return <CharacterCard compact isOwn {...rowData.character} />;
|
||||
};
|
||||
|
||||
export const renderValueTemplate = (rowData: ActivitySummary, field: keyof ActivitySummary) => {
|
||||
return <div className="tabular-nums w-full flex justify-center">{rowData[field] as number}</div>;
|
||||
};
|
||||
@@ -11,22 +11,16 @@ import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
export interface MapContextMenuProps {
|
||||
onShowOnTheMap?: () => void;
|
||||
onShowMapSettings?: () => void;
|
||||
onShowTrackingDialog?: () => void;
|
||||
}
|
||||
|
||||
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContextMenuProps) => {
|
||||
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
|
||||
const { outCommand, setInterfaceSettings } = useMapRootState();
|
||||
|
||||
const canTrackCharacters = useMapCheckPermissions([UserPermission.TRACK_CHARACTER]);
|
||||
|
||||
const menuRight = useRef<Menu>(null);
|
||||
|
||||
const handleAddCharacter = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
const handleShowActivity = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.showActivity,
|
||||
@@ -40,8 +34,8 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
{
|
||||
label: 'Tracking',
|
||||
icon: 'pi pi-user-plus',
|
||||
command: handleAddCharacter,
|
||||
visible: true,
|
||||
command: onShowTrackingDialog,
|
||||
visible: canTrackCharacters,
|
||||
},
|
||||
{
|
||||
label: 'Character Activity',
|
||||
@@ -76,7 +70,7 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContext
|
||||
).filter(item => item.visible);
|
||||
}, [
|
||||
canTrackCharacters,
|
||||
handleAddCharacter,
|
||||
onShowTrackingDialog,
|
||||
handleShowActivity,
|
||||
onShowMapSettings,
|
||||
onShowOnTheMap,
|
||||
|
||||
@@ -1,84 +1,3 @@
|
||||
.verticalTabsContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
|
||||
:global {
|
||||
.p-tabview {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.p-tabview-panels {
|
||||
padding: 6px 1rem !important;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav-container {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav {
|
||||
flex-direction: column;
|
||||
width: 150px;
|
||||
min-height: 100%;
|
||||
border: none;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
border-right: 4px solid var(--surface-hover);
|
||||
background-color: var(--surface-card);
|
||||
|
||||
transition: background-color 200ms, border-right-color 200ms;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--surface-100);
|
||||
}
|
||||
|
||||
.p-tabview-nav-link {
|
||||
transition: color 200ms;
|
||||
|
||||
justify-content: flex-end;
|
||||
padding: 10px;
|
||||
//background-color: var(--surface-card);
|
||||
background-color: initial;
|
||||
border: none;
|
||||
color: var(--gray-400);
|
||||
|
||||
border-radius: initial;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.p-tabview-selected {
|
||||
background-color: var(--surface-50);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
|
||||
.p-tabview-nav-link {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
//background-color: var(--surface-hover);
|
||||
border-right: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-tabview-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.CheckboxContainer {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
|
||||
@@ -1,210 +1,56 @@
|
||||
import styles from './MapSettings.module.scss';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { PrettySwitchbox } from './components';
|
||||
import {
|
||||
InterfaceStoredSettingsProps,
|
||||
useMapRootState,
|
||||
InterfaceStoredSettings,
|
||||
AvailableThemes
|
||||
} from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { WidgetsSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/WidgetsSettings/WidgetsSettings.tsx';
|
||||
|
||||
export enum UserSettingsRemoteProps {
|
||||
link_signature_on_splash = 'link_signature_on_splash',
|
||||
select_on_spash = 'select_on_spash',
|
||||
delete_connection_with_sigs = 'delete_connection_with_sigs',
|
||||
}
|
||||
|
||||
export const DEFAULT_REMOTE_SETTINGS = {
|
||||
[UserSettingsRemoteProps.link_signature_on_splash]: false,
|
||||
[UserSettingsRemoteProps.select_on_spash]: false,
|
||||
[UserSettingsRemoteProps.delete_connection_with_sigs]: false,
|
||||
};
|
||||
|
||||
export const UserSettingsRemoteList = [
|
||||
UserSettingsRemoteProps.link_signature_on_splash,
|
||||
UserSettingsRemoteProps.select_on_spash,
|
||||
UserSettingsRemoteProps.delete_connection_with_sigs,
|
||||
];
|
||||
|
||||
export type UserSettingsRemote = {
|
||||
link_signature_on_splash: boolean;
|
||||
select_on_spash: boolean;
|
||||
delete_connection_with_sigs: boolean;
|
||||
};
|
||||
|
||||
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
|
||||
import {
|
||||
CONNECTIONS_CHECKBOXES_PROPS,
|
||||
SIGNATURES_CHECKBOXES_PROPS,
|
||||
SYSTEMS_CHECKBOXES_PROPS,
|
||||
THEME_SETTING,
|
||||
UI_CHECKBOXES_PROPS,
|
||||
} from './constants.ts';
|
||||
import {
|
||||
MapSettingsProvider,
|
||||
useMapSettings,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
|
||||
import { WidgetsSettings } from './components/WidgetsSettings';
|
||||
import { CommonSettings } from './components/CommonSettings';
|
||||
import { SettingsListItem } from './types.ts';
|
||||
|
||||
export interface MapSettingsProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
type SettingsListItem = {
|
||||
prop: keyof UserSettings;
|
||||
label: string;
|
||||
type: 'checkbox' | 'dropdown';
|
||||
options?: { label: string; value: string }[];
|
||||
};
|
||||
|
||||
const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowMinimap,
|
||||
label: 'Show Minimap',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowKSpace,
|
||||
label: 'Highlight Low/High-security systems',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: UserSettingsRemoteProps.select_on_spash,
|
||||
label: 'Auto-select splashed',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: UserSettingsRemoteProps.link_signature_on_splash,
|
||||
label: 'Link signature on splash',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures,
|
||||
label: 'Show unsplashed signatures',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
|
||||
label: 'Delete connections to linked signatures',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isThickConnections,
|
||||
label: 'Thicker connections',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowMenu,
|
||||
label: 'Enable compact map menu bar',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowBackgroundPattern,
|
||||
label: 'Show background pattern',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isSoftBackground,
|
||||
label: 'Enable soft background',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ label: 'Default', value: AvailableThemes.default },
|
||||
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
|
||||
];
|
||||
|
||||
const THEME_SETTING: SettingsListItem = {
|
||||
prop: 'theme',
|
||||
label: 'Theme',
|
||||
type: 'dropdown',
|
||||
options: THEME_OPTIONS,
|
||||
};
|
||||
|
||||
export const MapSettings = ({ visible, onHide }: MapSettingsProps) => {
|
||||
export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
|
||||
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
|
||||
...DEFAULT_REMOTE_SETTINGS,
|
||||
});
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const mergedSettings = useMemo(() => {
|
||||
return {
|
||||
...userRemoteSettings,
|
||||
...interfaceSettings,
|
||||
};
|
||||
}, [userRemoteSettings, interfaceSettings]);
|
||||
const { renderSettingItem, setUserRemoteSettings } = useMapSettings();
|
||||
|
||||
const handleShow = async () => {
|
||||
const { user_settings } = await outCommand({
|
||||
const refVars = useRef({ outCommand, onHide, visible });
|
||||
refVars.current = { outCommand, onHide, visible };
|
||||
|
||||
const handleShow = useCallback(async () => {
|
||||
const { user_settings } = await refVars.current.outCommand({
|
||||
type: OutCommand.getUserSettings,
|
||||
data: null,
|
||||
});
|
||||
setUserRemoteSettings({
|
||||
...user_settings,
|
||||
});
|
||||
};
|
||||
}, [setUserRemoteSettings]);
|
||||
|
||||
const handleSettingChange = useCallback(
|
||||
async (prop: keyof UserSettings, value: boolean | string) => {
|
||||
if (UserSettingsRemoteList.includes(prop as any)) {
|
||||
const newRemoteSettings = {
|
||||
...userRemoteSettings,
|
||||
[prop]: value,
|
||||
};
|
||||
await outCommand({
|
||||
type: OutCommand.updateUserSettings,
|
||||
data: newRemoteSettings,
|
||||
});
|
||||
setUserRemoteSettings(newRemoteSettings);
|
||||
} else {
|
||||
setInterfaceSettings({
|
||||
...interfaceSettings,
|
||||
[prop]: value,
|
||||
});
|
||||
}
|
||||
},
|
||||
[userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings],
|
||||
);
|
||||
|
||||
const renderSettingItem = (item: SettingsListItem) => {
|
||||
const currentValue = mergedSettings[item.prop];
|
||||
|
||||
if (item.type === 'checkbox') {
|
||||
return (
|
||||
<PrettySwitchbox
|
||||
key={item.prop}
|
||||
label={item.label}
|
||||
checked={!!currentValue}
|
||||
setChecked={checked => handleSettingChange(item.prop, checked)}
|
||||
/>
|
||||
);
|
||||
const handleHide = useCallback(() => {
|
||||
if (!refVars.current.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.type === 'dropdown' && item.options) {
|
||||
return (
|
||||
<div key={item.prop} className="flex items-center gap-2 mt-2">
|
||||
<label className="text-sm">{item.label}:</label>
|
||||
<Dropdown
|
||||
className="text-sm"
|
||||
value={currentValue}
|
||||
options={item.options}
|
||||
onChange={e => handleSettingChange(item.prop, e.value)}
|
||||
placeholder="Select a theme"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
setActiveIndex(0);
|
||||
refVars.current.onHide();
|
||||
}, []);
|
||||
|
||||
const renderSettingsList = (list: SettingsListItem[]) => {
|
||||
return list.map(renderSettingItem);
|
||||
@@ -217,47 +63,53 @@ export const MapSettings = ({ visible, onHide }: MapSettingsProps) => {
|
||||
draggable={false}
|
||||
style={{ width: '550px' }}
|
||||
onShow={handleShow}
|
||||
onHide={() => {
|
||||
if (!visible) return;
|
||||
setActiveIndex(0);
|
||||
onHide();
|
||||
}}
|
||||
onHide={handleHide}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className={styles.verticalTabsContainer}>
|
||||
<TabView activeIndex={activeIndex} onTabChange={e => setActiveIndex(e.index)}>
|
||||
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>
|
||||
</TabPanel>
|
||||
<TabView
|
||||
activeIndex={activeIndex}
|
||||
className="vertical-tabs-container"
|
||||
onTabChange={e => setActiveIndex(e.index)}
|
||||
>
|
||||
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
|
||||
<CommonSettings />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}</div>
|
||||
</TabPanel>
|
||||
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(SYSTEMS_CHECKBOXES_PROPS)}</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Connections" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingsList(CONNECTIONS_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
<TabPanel header="Connections" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingsList(CONNECTIONS_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
<TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingsList(SIGNATURES_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingsList(UI_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="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
|
||||
<WidgetsSettings />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingItem(THEME_SETTING)}
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</div>
|
||||
<TabPanel header="Theme" headerClassName={styles.verticalTabHeader}>
|
||||
{renderSettingItem(THEME_SETTING)}
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const MapSettings = (props: MapSettingsProps) => {
|
||||
return (
|
||||
<MapSettingsProvider>
|
||||
<MapSettingsComp {...props} />
|
||||
</MapSettingsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
SettingsListItem,
|
||||
UserSettings,
|
||||
UserSettingsRemote,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
|
||||
import {
|
||||
DEFAULT_REMOTE_SETTINGS,
|
||||
UserSettingsRemoteList,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/constants.ts';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
type MapSettingsContextType = {
|
||||
renderSettingItem: (item: SettingsListItem) => ReactNode;
|
||||
setUserRemoteSettings: Dispatch<SetStateAction<UserSettingsRemote>>;
|
||||
};
|
||||
|
||||
const MapSettingsContext = createContext<MapSettingsContextType | undefined>(undefined);
|
||||
|
||||
export const MapSettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
|
||||
|
||||
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({
|
||||
...DEFAULT_REMOTE_SETTINGS,
|
||||
});
|
||||
|
||||
const mergedSettings = useMemo(() => {
|
||||
return {
|
||||
...userRemoteSettings,
|
||||
...interfaceSettings,
|
||||
};
|
||||
}, [userRemoteSettings, interfaceSettings]);
|
||||
|
||||
const refVars = useRef({ mergedSettings, userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings });
|
||||
refVars.current = { mergedSettings, userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings };
|
||||
|
||||
const handleSettingChange = useCallback(async (prop: keyof UserSettings, value: boolean | string) => {
|
||||
const { userRemoteSettings, interfaceSettings, outCommand, setInterfaceSettings } = refVars.current;
|
||||
|
||||
if (UserSettingsRemoteList.includes(prop as any)) {
|
||||
const newRemoteSettings = {
|
||||
...userRemoteSettings,
|
||||
[prop]: value,
|
||||
};
|
||||
await outCommand({
|
||||
type: OutCommand.updateUserSettings,
|
||||
data: newRemoteSettings,
|
||||
});
|
||||
setUserRemoteSettings(newRemoteSettings);
|
||||
} else {
|
||||
setInterfaceSettings({
|
||||
...interfaceSettings,
|
||||
[prop]: value,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderSettingItem = useCallback(
|
||||
(item: SettingsListItem) => {
|
||||
const currentValue = refVars.current.mergedSettings[item.prop];
|
||||
|
||||
if (item.type === 'checkbox') {
|
||||
return (
|
||||
<PrettySwitchbox
|
||||
key={item.prop}
|
||||
label={item.label}
|
||||
checked={!!currentValue}
|
||||
setChecked={checked => handleSettingChange(item.prop, checked)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === 'dropdown' && item.options) {
|
||||
return (
|
||||
<div key={item.prop} className="flex items-center gap-2 mt-2">
|
||||
<label className="text-sm">{item.label}:</label>
|
||||
<Dropdown
|
||||
className="text-sm"
|
||||
value={currentValue}
|
||||
options={item.options}
|
||||
onChange={e => handleSettingChange(item.prop, e.value)}
|
||||
placeholder="Select a theme"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[handleSettingChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<MapSettingsContext.Provider value={{ renderSettingItem, setUserRemoteSettings }}>
|
||||
{children}
|
||||
</MapSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMapSettings = () => {
|
||||
const context = useContext(MapSettingsContext);
|
||||
if (!context) {
|
||||
throw new Error('useMapSettings must be used within a MapSettingsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
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';
|
||||
|
||||
export const CommonSettings = () => {
|
||||
const { renderSettingItem } = useMapSettings();
|
||||
|
||||
const renderSettingsList = (list: SettingsListItem[]) => {
|
||||
return list.map(renderSettingItem);
|
||||
};
|
||||
|
||||
return <div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/index.ts';
|
||||
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback } from 'react';
|
||||
@@ -0,0 +1,91 @@
|
||||
import { SettingsListItem, UserSettingsRemoteProps } from './types.ts';
|
||||
import { AvailableThemes, InterfaceStoredSettingsProps } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const DEFAULT_REMOTE_SETTINGS = {
|
||||
[UserSettingsRemoteProps.link_signature_on_splash]: false,
|
||||
[UserSettingsRemoteProps.select_on_spash]: false,
|
||||
[UserSettingsRemoteProps.delete_connection_with_sigs]: false,
|
||||
};
|
||||
|
||||
export const UserSettingsRemoteList = [
|
||||
UserSettingsRemoteProps.link_signature_on_splash,
|
||||
UserSettingsRemoteProps.select_on_spash,
|
||||
UserSettingsRemoteProps.delete_connection_with_sigs,
|
||||
];
|
||||
|
||||
export const COMMON_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowMinimap,
|
||||
label: 'Show Minimap',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
export const SYSTEMS_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowKSpace,
|
||||
label: 'Highlight Low/High-security systems',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: UserSettingsRemoteProps.select_on_spash,
|
||||
label: 'Auto-select splashed',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
export const SIGNATURES_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: UserSettingsRemoteProps.link_signature_on_splash,
|
||||
label: 'Link signature on splash',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowUnsplashedSignatures,
|
||||
label: 'Show unsplashed signatures',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
export const CONNECTIONS_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: UserSettingsRemoteProps.delete_connection_with_sigs,
|
||||
label: 'Delete connections to linked signatures',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isThickConnections,
|
||||
label: 'Thicker connections',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
export const UI_CHECKBOXES_PROPS: SettingsListItem[] = [
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowMenu,
|
||||
label: 'Enable compact map menu bar',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isShowBackgroundPattern,
|
||||
label: 'Show background pattern',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
prop: InterfaceStoredSettingsProps.isSoftBackground,
|
||||
label: 'Enable soft background',
|
||||
type: 'checkbox',
|
||||
},
|
||||
];
|
||||
|
||||
export const THEME_OPTIONS = [
|
||||
{ label: 'Default', value: AvailableThemes.default },
|
||||
{ label: 'Pathfinder', value: AvailableThemes.pathfinder },
|
||||
];
|
||||
|
||||
export const THEME_SETTING: SettingsListItem = {
|
||||
prop: 'theme',
|
||||
label: 'Theme',
|
||||
type: 'dropdown',
|
||||
options: THEME_OPTIONS,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { InterfaceStoredSettings } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export enum UserSettingsRemoteProps {
|
||||
link_signature_on_splash = 'link_signature_on_splash',
|
||||
select_on_spash = 'select_on_spash',
|
||||
delete_connection_with_sigs = 'delete_connection_with_sigs',
|
||||
}
|
||||
|
||||
export type UserSettingsRemote = {
|
||||
link_signature_on_splash: boolean;
|
||||
select_on_spash: boolean;
|
||||
delete_connection_with_sigs: boolean;
|
||||
};
|
||||
|
||||
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
|
||||
|
||||
export type SettingsListItem = {
|
||||
prop: keyof UserSettings;
|
||||
label: string;
|
||||
type: 'checkbox' | 'dropdown';
|
||||
options?: { label: string; value: string }[];
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import classes from './RightBar.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
@@ -12,23 +11,16 @@ import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
interface RightBarProps {
|
||||
onShowOnTheMap?: () => void;
|
||||
onShowMapSettings?: () => void;
|
||||
onShowTrackingDialog?: () => void;
|
||||
}
|
||||
|
||||
export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) => {
|
||||
const { outCommand, 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 handleShowTracking = useCallback(() => {
|
||||
// Use the OutCommand pattern for showing the tracking dialog
|
||||
outCommand({
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand]);
|
||||
|
||||
const toggleMinimap = useCallback(() => {
|
||||
setInterfaceSettings(x => ({
|
||||
...x,
|
||||
@@ -60,19 +52,19 @@ export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) =
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2 items-center mt-1">
|
||||
<WdTooltipWrapper content="Tracking status" 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={handleShowTracking}
|
||||
id="show-tracking-button"
|
||||
>
|
||||
<i className="pi pi-user-plus"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
{canTrackCharacters && (
|
||||
<>
|
||||
<WdTooltipWrapper content="Tracking status" 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={onShowTrackingDialog}
|
||||
id="show-tracking-button"
|
||||
>
|
||||
<i className="pi pi-user-plus"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.trackFollowHeader {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { VirtualScroller } from 'primereact/virtualscroller';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TrackingCharacter } from './types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
|
||||
|
||||
interface TrackAndFollowProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
|
||||
const { outCommand, data } = useMapRootState();
|
||||
const { trackingCharactersData } = data;
|
||||
|
||||
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
|
||||
|
||||
const handleTrackToggle = useCallback(
|
||||
async (characterId: string) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { character_id: characterId },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling track:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const handleFollowToggle = useCallback(
|
||||
async (characterId: string) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { character_id: characterId },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling follow:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const rowTemplate = (tc: TrackingCharacter) => {
|
||||
const characterEveId = tc.character.eve_id;
|
||||
|
||||
return (
|
||||
<TrackingCharacterWrapper
|
||||
key={characterEveId}
|
||||
character={tc.character}
|
||||
isTracked={tc.tracked}
|
||||
isFollowed={tc.followed}
|
||||
onTrackToggle={() => handleTrackToggle(characterEveId)}
|
||||
onFollowToggle={() => handleFollowToggle(characterEveId)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={
|
||||
<div className="dialog-header">
|
||||
<span>Track & Follow</span>
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
className="w-[500px] text-text-color"
|
||||
contentClassName="!p-0"
|
||||
>
|
||||
<div className="w-full overflow-hidden">
|
||||
<div className="grid grid-cols-[80px_80px_1fr] p-1 font-normal text-sm text-center border-b border-[#383838]">
|
||||
<div>Track</div>
|
||||
<div>Follow</div>
|
||||
<div className="text-center">Character</div>
|
||||
</div>
|
||||
<VirtualScroller items={characters} itemSize={48} itemTemplate={rowTemplate} className="h-72 w-full" />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
.characterRow {
|
||||
border-color: var(--surface-border);
|
||||
border-width: 0 0 1px 0;
|
||||
border-style: solid;
|
||||
opacity: 0.5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip/WdTooltip';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
|
||||
|
||||
interface TrackingCharacterWrapperProps {
|
||||
character: CharacterTypeRaw;
|
||||
isTracked: boolean;
|
||||
isFollowed: boolean;
|
||||
onTrackToggle: () => void;
|
||||
onFollowToggle: () => void;
|
||||
}
|
||||
|
||||
export const TrackingCharacterWrapper = ({
|
||||
character,
|
||||
isTracked,
|
||||
isFollowed,
|
||||
onTrackToggle,
|
||||
onFollowToggle,
|
||||
}: TrackingCharacterWrapperProps) => {
|
||||
const trackCheckboxId = `track-${character.eve_id}`;
|
||||
const followRadioId = `follow-${character.eve_id}`;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[80px_80px_1fr] items-center min-h-8 hover:bg-neutral-800 border-b border-[#383838]">
|
||||
<div className="flex justify-center items-center p-0.5 text-center">
|
||||
<WdTooltipWrapper content="Track this character on the map" position={TooltipPosition.top}>
|
||||
<div className="flex justify-center items-center w-full">
|
||||
<WdCheckbox id={trackCheckboxId} label="" value={isTracked} onChange={onTrackToggle} />
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
<div className="flex justify-center items-center p-0.5 text-center">
|
||||
<WdTooltipWrapper content="Follow this character's movements on the map" position={TooltipPosition.top}>
|
||||
<div className="flex justify-center items-center w-full">
|
||||
<div onClick={onFollowToggle} className="cursor-pointer">
|
||||
<WdRadioButton id={followRadioId} name="followed_character" checked={isFollowed} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<CharacterCard showShipName={false} showSystem={false} isOwn {...character} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
/**
|
||||
* Interface for a character that can be tracked and followed
|
||||
*/
|
||||
export interface TrackingCharacter {
|
||||
character: CharacterTypeRaw;
|
||||
tracked: boolean;
|
||||
followed: boolean;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Column } from 'primereact/column';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
|
||||
|
||||
export const TrackingCharactersList = () => {
|
||||
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
|
||||
const { trackingCharacters, updateTracking } = useTracking();
|
||||
const refVars = useRef({ trackingCharacters });
|
||||
refVars.current = { trackingCharacters };
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(trackingCharacters.filter(x => x.tracked));
|
||||
}, [trackingCharacters]);
|
||||
|
||||
const handleChangeSelect = useCallback(
|
||||
(selected: TrackingCharacter[]) => updateTracking(selected.map(x => x.character.eve_id)),
|
||||
[updateTracking],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
value={trackingCharacters}
|
||||
size="small"
|
||||
selectionMode={null}
|
||||
selection={selected}
|
||||
onSelectionChange={e => handleChangeSelect(e.value)}
|
||||
virtualScrollerOptions={{ itemSize: 40 }}
|
||||
className="relative w-full select-none min-h-0 h-full"
|
||||
resizableColumns={false}
|
||||
rowHover
|
||||
selectAll
|
||||
>
|
||||
<Column
|
||||
selectionMode="multiple"
|
||||
headerClassName="h-[40px] !pl-4"
|
||||
className="w-12 max-w-12 !pl-4 [&_div]:mt-[-2px] "
|
||||
/>
|
||||
<Column
|
||||
field="eve_id"
|
||||
header="Character with tracking access"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
headerClassName="[&_div]:ml-2"
|
||||
body={row => {
|
||||
return <CharacterCard showShipName={false} showSystem={false} isOwn {...row.character} />;
|
||||
}}
|
||||
/>
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { TabPanel, TabView } from 'primereact/tabview';
|
||||
import { TrackingSettings } from './TrackingSettings.tsx';
|
||||
import { TrackingCharactersList } from './TrackingCharactersList.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TrackingProvider, useTracking } from './TrackingProvider.tsx';
|
||||
|
||||
interface TrackingDialogProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const TrackingDialogComp = ({ visible, onHide }: TrackingDialogProps) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const { outCommand } = useMapRootState();
|
||||
const { loadTracking } = useTracking();
|
||||
|
||||
const refVars = useRef({ outCommand });
|
||||
refVars.current = { outCommand };
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadTracking();
|
||||
}, [loadTracking, visible]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header={
|
||||
<div className="dialog-header">
|
||||
<span className="pointer-events-none">Track & Follow</span>
|
||||
</div>
|
||||
}
|
||||
draggable={false}
|
||||
resizable={false}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
className="w-[640px] h-[400px] text-text-color min-h-0"
|
||||
>
|
||||
<TabView
|
||||
className="vertical-tabs-container h-full [&_.p-tabview-panels]:!pr-0"
|
||||
activeIndex={activeIndex}
|
||||
onTabChange={e => setActiveIndex(e.index)}
|
||||
renderActiveOnly={false}
|
||||
>
|
||||
<TabPanel header="Tracking" contentClassName="h-full">
|
||||
<TrackingCharactersList />
|
||||
</TabPanel>
|
||||
<TabPanel header="Follow & Settings">
|
||||
<TrackingSettings />
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrackingDialog = (props: TrackingDialogProps) => {
|
||||
return (
|
||||
<TrackingProvider>
|
||||
<TrackingDialogComp {...props} />
|
||||
</TrackingProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
import { createContext, useCallback, useContext, useRef, useState } from 'react';
|
||||
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
|
||||
|
||||
type DiffTrackingInfo = { characterId: string; tracked: boolean };
|
||||
|
||||
type TrackingContextType = {
|
||||
loadTracking: () => void;
|
||||
updateTracking: (selected: string[]) => void;
|
||||
updateFollowing: (characterId: string | null) => void;
|
||||
updateMain: (characterId: string) => void;
|
||||
trackingCharacters: TrackingCharacter[];
|
||||
following: string | null;
|
||||
main: string | null;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
const TrackingContext = createContext<TrackingContextType | undefined>(undefined);
|
||||
|
||||
export const TrackingProvider = ({ children }: WithChildren) => {
|
||||
const [trackingCharacters, setTrackingCharacters] = useState<TrackingCharacter[]>([]);
|
||||
const [following, setFollowing] = useState<string | null>(null);
|
||||
const [main, setMain] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
const refVars = useRef({ outCommand, trackingCharacters, following });
|
||||
refVars.current = { outCommand, trackingCharacters, following };
|
||||
|
||||
const loadTracking = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res: IncomingEvent<CommandInCharactersTrackingInfo> = await refVars.current.outCommand({
|
||||
type: OutCommand.getCharactersTrackingInfo,
|
||||
data: {},
|
||||
});
|
||||
|
||||
setTrackingCharacters(res.data.characters);
|
||||
setFollowing(res.data.following);
|
||||
setMain(res.data.main);
|
||||
} catch (err) {
|
||||
console.error('TrackingProviderError', err);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const changeTrackingCommand = useCallback(
|
||||
async (characterId: string, track: boolean) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.updateCharacterTracking,
|
||||
data: { character_eve_id: characterId, track },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling track:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const updateFollowing = useCallback(
|
||||
async (characterId: string | null) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.updateFollowingCharacter,
|
||||
data: { character_eve_id: characterId },
|
||||
});
|
||||
setFollowing(characterId);
|
||||
} catch (error) {
|
||||
console.error('Error toggling follow:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const updateTracking = useCallback(
|
||||
async (selected: string[]) => {
|
||||
const { following, trackingCharacters } = refVars.current;
|
||||
const diffToUpdate: DiffTrackingInfo[] = [];
|
||||
|
||||
const newVal = trackingCharacters.map(x => {
|
||||
const next = selected.includes(x.character.eve_id);
|
||||
|
||||
if (next !== x.tracked) {
|
||||
diffToUpdate.push({ characterId: x.character.eve_id, tracked: next });
|
||||
}
|
||||
|
||||
return {
|
||||
tracked: selected.includes(x.character.eve_id),
|
||||
character: x.character,
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(diffToUpdate.map(x => changeTrackingCommand(x.characterId, x.tracked)));
|
||||
|
||||
if (newVal.some(x => following != null && x.character.eve_id === following && !x.tracked)) {
|
||||
await updateFollowing(null);
|
||||
setFollowing(null);
|
||||
}
|
||||
|
||||
setTrackingCharacters(newVal);
|
||||
},
|
||||
[changeTrackingCommand, updateFollowing],
|
||||
);
|
||||
|
||||
const updateMain = useCallback(
|
||||
async (characterId: string) => {
|
||||
try {
|
||||
await outCommand({
|
||||
type: OutCommand.updateMainCharacter,
|
||||
data: { character_eve_id: characterId },
|
||||
});
|
||||
setMain(characterId);
|
||||
} catch (error) {
|
||||
console.error('Error toggling main:', error);
|
||||
}
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
return (
|
||||
<TrackingContext.Provider
|
||||
value={{
|
||||
loadTracking,
|
||||
trackingCharacters,
|
||||
following,
|
||||
main,
|
||||
loading,
|
||||
updateTracking,
|
||||
updateFollowing,
|
||||
updateMain,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TrackingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTracking = () => {
|
||||
const context = useContext(TrackingContext);
|
||||
if (!context) {
|
||||
throw new Error('useTracking must be used within a TrackingProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
|
||||
|
||||
const renderValCharacterTemplate = (row: TrackingCharacter | undefined) => {
|
||||
if (!row) {
|
||||
return <div className="h-[26px] flex items-center">Character is not selected</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-1">
|
||||
<CharacterCard compact showShipName={false} showSystem={false} isOwn {...row.character} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCharacterTemplate = (row: TrackingCharacter | undefined) => {
|
||||
if (!row) {
|
||||
return <div className="h-[33px] flex items-center">Character is not selected</div>;
|
||||
}
|
||||
|
||||
return <CharacterCard showShipName={false} showSystem={false} isOwn {...row.character} />;
|
||||
};
|
||||
|
||||
export const TrackingSettings = () => {
|
||||
const { trackingCharacters, following, main, updateFollowing, updateMain } = useTracking();
|
||||
|
||||
const followingChar = useMemo(
|
||||
() => trackingCharacters.filter(x => x.tracked).find(x => x.character.eve_id === following),
|
||||
[following, trackingCharacters],
|
||||
);
|
||||
|
||||
const availableForFollowingCharacters = useMemo(
|
||||
() => trackingCharacters.filter(x => x.tracked),
|
||||
[trackingCharacters],
|
||||
);
|
||||
|
||||
const mainChar = useMemo(() => trackingCharacters.find(x => x.character.eve_id === main), [main, trackingCharacters]);
|
||||
|
||||
const handleSelectFollowing = useCallback(
|
||||
(e: TrackingCharacter | null) => updateFollowing(e == null ? null : e.character.eve_id),
|
||||
[updateFollowing],
|
||||
);
|
||||
|
||||
const handleSelectMain = useCallback((e: TrackingCharacter) => updateMain(e.character.eve_id), [updateMain]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-2 mx-2">
|
||||
<label className="text-stone-400 text-[13px] select-none">Main character</label>
|
||||
<Dropdown
|
||||
options={trackingCharacters}
|
||||
value={mainChar}
|
||||
onChange={e => handleSelectMain(e.value)}
|
||||
className="w-[230px]"
|
||||
itemTemplate={renderCharacterTemplate}
|
||||
valueTemplate={renderValCharacterTemplate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 mx-2">
|
||||
<label className="text-stone-400 text-[13px] select-none">Following character</label>
|
||||
<Dropdown
|
||||
disabled={availableForFollowingCharacters.length === 0}
|
||||
options={availableForFollowingCharacters}
|
||||
value={followingChar}
|
||||
onChange={e => handleSelectFollowing(e.value)}
|
||||
className="w-[230px]"
|
||||
itemTemplate={renderCharacterTemplate}
|
||||
valueTemplate={renderValCharacterTemplate}
|
||||
showClear
|
||||
placeholder="Character is not selected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TrackingDialog.tsx';
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, CommandData, Commands } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import type { TrackingCharacter } from '@/hooks/Mapper/components/mapRootContent/components/TrackAndFollow/types';
|
||||
|
||||
/**
|
||||
* Hook for track and follow related handlers
|
||||
*/
|
||||
export const useTrackAndFollowHandlers = () => {
|
||||
const { outCommand, update } = useMapRootState();
|
||||
|
||||
/**
|
||||
* Handle hiding the track and follow dialog
|
||||
*/
|
||||
const handleHideTracking = useCallback(() => {
|
||||
// Then update local state to hide the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: false,
|
||||
}));
|
||||
}, [update]);
|
||||
|
||||
/**
|
||||
* Handle showing the track and follow dialog
|
||||
*/
|
||||
const handleShowTracking = useCallback(() => {
|
||||
// Update local state to show the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
outCommand({
|
||||
type: OutCommand.showTracking,
|
||||
data: {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
/**
|
||||
* Handle updating tracking data
|
||||
*/
|
||||
const handleUpdateTracking = useCallback(
|
||||
(trackingData: { characters: TrackingCharacter[] }) => {
|
||||
if (!trackingData || !trackingData.characters) {
|
||||
console.error('Invalid tracking data received:', trackingData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state with the tracking data
|
||||
update(state => ({
|
||||
...state,
|
||||
trackingCharactersData: trackingData.characters,
|
||||
showTrackAndFollow: true,
|
||||
}));
|
||||
},
|
||||
[update],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle toggling character tracking
|
||||
*/
|
||||
const handleToggleTrack = useCallback(
|
||||
(characterId: string) => {
|
||||
if (!characterId) return;
|
||||
|
||||
// Send the toggle track command to the server
|
||||
outCommand({
|
||||
type: OutCommand.toggleTrack,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
|
||||
// Note: The local state is now updated in the TrackAndFollow component
|
||||
// for immediate UI feedback, while we wait for the server response
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle toggling character following
|
||||
*/
|
||||
const handleToggleFollow = useCallback(
|
||||
(characterId: string) => {
|
||||
if (!characterId) return;
|
||||
|
||||
// Send the toggle follow command to the server
|
||||
outCommand({
|
||||
type: OutCommand.toggleFollow,
|
||||
data: { 'character-id': characterId },
|
||||
});
|
||||
|
||||
// Note: The local state is now updated in the TrackAndFollow component
|
||||
// for immediate UI feedback, while we wait for the server response
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle user settings updates
|
||||
*/
|
||||
const handleUserSettingsUpdated = useCallback((settingsData: CommandData[Commands.userSettingsUpdated]) => {
|
||||
if (!settingsData || !settingsData.settings) {
|
||||
console.error('Invalid settings data received:', settingsData);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleHideTracking,
|
||||
handleShowTracking,
|
||||
handleUpdateTracking,
|
||||
handleToggleTrack,
|
||||
handleToggleFollow,
|
||||
handleUserSettingsUpdated,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
|
||||
import { useCallback, 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';
|
||||
@@ -34,13 +34,14 @@ export const MapWrapper = () => {
|
||||
const {
|
||||
update,
|
||||
outCommand,
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem },
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, linkSignatureToSystem, systemSignatures },
|
||||
interfaceSettings: {
|
||||
isShowMenu,
|
||||
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
|
||||
isShowKSpace,
|
||||
isThickConnections,
|
||||
isShowBackgroundPattern,
|
||||
isShowUnsplashedSignatures,
|
||||
isSoftBackground,
|
||||
theme,
|
||||
},
|
||||
@@ -58,8 +59,15 @@ export const MapWrapper = () => {
|
||||
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
|
||||
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
|
||||
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
|
||||
const ref = useRef({
|
||||
selectedConnections,
|
||||
selectedSystems,
|
||||
systemContextProps,
|
||||
systems,
|
||||
systemSignatures,
|
||||
deleteSystems,
|
||||
});
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, systemSignatures, deleteSystems };
|
||||
|
||||
useMapEventListener(event => {
|
||||
runCommand(event);
|
||||
@@ -128,7 +136,7 @@ export const MapWrapper = () => {
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => {
|
||||
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
|
||||
if (ref.current.systems.some(x => parseInt(x.id) === item.value)) {
|
||||
emitMapEvent({
|
||||
name: Commands.centerSystem,
|
||||
data: item.value.toString(),
|
||||
@@ -156,6 +164,15 @@ export const MapWrapper = () => {
|
||||
handleDeleteSelected();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { systemSignatures, systems } = ref.current;
|
||||
if (!isShowUnsplashedSignatures || Object.keys(systemSignatures).length !== 0 || systems?.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
outCommand({ type: OutCommand.loadSignatures, data: {} });
|
||||
}, [isShowUnsplashedSignatures, systems]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Map
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Characters from '../characters/Characters';
|
||||
import { Characters } from '../characters/Characters';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.Docked {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 1px;
|
||||
|
||||
background-image: url(/images/citadelLarge.png);
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
transform: rotateZ(0deg);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ 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';
|
||||
|
||||
type CharacterCardProps = {
|
||||
compact?: boolean;
|
||||
@@ -45,8 +47,9 @@ export const CharacterCard = ({
|
||||
if (compact) {
|
||||
return (
|
||||
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
|
||||
<div className="w-full flex items-center gap-1">
|
||||
<div className="w-full flex items-center gap-1 relative">
|
||||
<CharacterPortrait characterEveId={char.eve_id} size={CharacterPortraitSize.w18} />
|
||||
{isDocked(char.location) && <span className={classes.Docked} />}
|
||||
<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>{' '}
|
||||
|
||||
@@ -39,259 +39,262 @@ interface TriggerInfo {
|
||||
|
||||
const LEAVE_DELAY = 100;
|
||||
|
||||
export const WdTooltip = forwardRef(function WdTooltip(
|
||||
{
|
||||
content,
|
||||
targetSelector,
|
||||
position: tPosition = TooltipPosition.default,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
ref: ForwardedRef<WdTooltipHandlers>,
|
||||
) {
|
||||
// Always initialize position so we never have a null value.
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
export const WdTooltip = forwardRef(
|
||||
(
|
||||
{
|
||||
content,
|
||||
targetSelector,
|
||||
position: tPosition = TooltipPosition.default,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
ref: ForwardedRef<WdTooltipHandlers>,
|
||||
) => {
|
||||
// Always initialize position so we never have a null value.
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
|
||||
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
|
||||
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
|
||||
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 10;
|
||||
}
|
||||
if (newTop < 0) {
|
||||
newTop = 10;
|
||||
}
|
||||
|
||||
const rightEdge = newLeft + tooltipWidth + 10;
|
||||
if (rightEdge > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
const rightEdge = newLeft + tooltipWidth + 10;
|
||||
if (rightEdge > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
|
||||
const bottomEdge = newTop + tooltipHeight + 10;
|
||||
if (bottomEdge > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
const bottomEdge = newTop + tooltipHeight + 10;
|
||||
if (bottomEdge > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
|
||||
return { left: newLeft, top: newTop };
|
||||
}, []);
|
||||
return { left: newLeft, top: newTop };
|
||||
}, []);
|
||||
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (!interactive) {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
return;
|
||||
}
|
||||
if (!hideTimeoutRef.current) {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (!interactive) {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
}, [interactive]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (e?: React.MouseEvent) => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
if (e) {
|
||||
// Use e.currentTarget (or fallback to e.target) to determine the trigger element.
|
||||
const triggerEl = (e.currentTarget as HTMLElement) || (e.target as HTMLElement);
|
||||
if (triggerEl) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
|
||||
setPos(calcTooltipPosition({ x: e.clientX, y: e.clientY }));
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
},
|
||||
hide: () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !triggerInfo) return;
|
||||
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const { rect } = triggerInfo;
|
||||
let x = triggerInfo.clientX;
|
||||
let y = triggerInfo.clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
x = rect.left - tooltipBounds.width - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default case: use stored coordinates.
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetSelector) return;
|
||||
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
const targetEl = evt.target as HTMLElement | null;
|
||||
if (!targetEl) {
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerEl = targetEl.closest(targetSelector);
|
||||
const insideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
||||
|
||||
if (!triggerEl && !insideTooltip) {
|
||||
scheduleHide();
|
||||
return;
|
||||
if (!hideTimeoutRef.current) {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
}, [interactive]);
|
||||
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left:
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
break;
|
||||
case TooltipPosition.right:
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
break;
|
||||
case TooltipPosition.top:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
break;
|
||||
case TooltipPosition.bottom:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}
|
||||
};
|
||||
|
||||
const debounced = debounce(handleMouseMove, 15);
|
||||
|
||||
document.addEventListener('mousemove', debounced);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', debounced);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute px-2 py-1',
|
||||
'border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
className,
|
||||
pos === null ? 'invisible' : '',
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (interactive && hideTimeoutRef.current) {
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (e?: React.MouseEvent) => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
setIsMouseInsideTooltip(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsMouseInsideTooltip(false);
|
||||
if (interactive) {
|
||||
scheduleHide();
|
||||
if (e) {
|
||||
// Use e.currentTarget (or fallback to e.target) to determine the trigger element.
|
||||
const triggerEl = (e.currentTarget as HTMLElement) || (e.target as HTMLElement);
|
||||
if (triggerEl) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
|
||||
}
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
});
|
||||
setVisible(true);
|
||||
},
|
||||
hide: () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
setVisible(false);
|
||||
setPos(null);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !triggerInfo) return;
|
||||
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const { rect } = triggerInfo;
|
||||
let x = triggerInfo.clientX;
|
||||
let y = triggerInfo.clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
x = rect.left - tooltipBounds.width - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default case: use stored coordinates.
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetSelector) return;
|
||||
|
||||
const handleMouseMove = (evt: MouseEvent) => {
|
||||
const targetEl = evt.target as HTMLElement | null;
|
||||
if (!targetEl) {
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerEl = targetEl.closest(targetSelector);
|
||||
const insideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
||||
|
||||
if (!triggerEl && !insideTooltip) {
|
||||
scheduleHide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left:
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
break;
|
||||
case TooltipPosition.right:
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
break;
|
||||
case TooltipPosition.top:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
break;
|
||||
case TooltipPosition.bottom:
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}
|
||||
};
|
||||
|
||||
const debounced = debounce(handleMouseMove, 15);
|
||||
|
||||
document.addEventListener('mousemove', debounced);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', debounced);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute px-1 py-1',
|
||||
'border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos == null && 'invisible',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (interactive && hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
setIsMouseInsideTooltip(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsMouseInsideTooltip(false);
|
||||
if (interactive) {
|
||||
scheduleHide();
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
WdTooltip.displayName = 'WdTooltip';
|
||||
|
||||
@@ -2,18 +2,21 @@ import { forwardRef, HTMLProps, ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltip, WdTooltipHandlers, TooltipProps } from '@/hooks/Mapper/components/ui-kit';
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
|
||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
import { sizeClass, TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
|
||||
export type WdTooltipWrapperProps = {
|
||||
content?: (() => ReactNode) | ReactNode;
|
||||
size?: TooltipSize;
|
||||
interactive?: boolean;
|
||||
tooltipClassName?: string;
|
||||
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
({ className, children, content, offset, position, targetSelector, interactive, size, ...props }, forwardedRef) => {
|
||||
(
|
||||
{ className, children, content, offset, position, targetSelector, interactive, size, tooltipClassName, ...props },
|
||||
forwardedRef,
|
||||
) => {
|
||||
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
|
||||
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
|
||||
const finalTargetSelector = targetSelector || `.${autoClass}`;
|
||||
@@ -29,7 +32,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
|
||||
content={content}
|
||||
interactive={interactive}
|
||||
targetSelector={finalTargetSelector}
|
||||
className={size ? sizeClass(size) : undefined}
|
||||
className={clsx(size && sizeClass(size), tooltipClassName)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -37,18 +40,3 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
|
||||
);
|
||||
|
||||
WdTooltipWrapper.displayName = 'WdTooltipWrapper';
|
||||
|
||||
function sizeClass(size: TooltipSize) {
|
||||
switch (size) {
|
||||
case 'xs':
|
||||
return classes.wdTooltipSizeXs;
|
||||
case 'sm':
|
||||
return classes.wdTooltipSizeSm;
|
||||
case 'md':
|
||||
return classes.wdTooltipSizeMd;
|
||||
case 'lg':
|
||||
return classes.wdTooltipSizeLg;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import classes from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/WdTooltipWrapper.module.scss';
|
||||
|
||||
export enum TooltipSize {
|
||||
xs = 'xs',
|
||||
sm = 'sm',
|
||||
md = 'md',
|
||||
lg = 'lg',
|
||||
}
|
||||
|
||||
export const sizeClass = (size: TooltipSize) => {
|
||||
switch (size) {
|
||||
case TooltipSize.xs:
|
||||
return classes.wdTooltipSizeXs;
|
||||
case TooltipSize.sm:
|
||||
return classes.wdTooltipSizeSm;
|
||||
case TooltipSize.md:
|
||||
return classes.wdTooltipSizeMd;
|
||||
case TooltipSize.lg:
|
||||
return classes.wdTooltipSizeLg;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
9
assets/js/hooks/Mapper/helpers/isDocked.ts
Normal file
9
assets/js/hooks/Mapper/helpers/isDocked.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LocationRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
export const isDocked = (location: LocationRaw | null) => {
|
||||
if (!location) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return location.station_id != null || location.structure_id != null;
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext, useEffect } from 'react';
|
||||
import {
|
||||
ActivitySummary,
|
||||
CommandLinkSignatureToSystem,
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
TrackingCharacter,
|
||||
UseCharactersCacheData,
|
||||
UseCommentsData,
|
||||
} from '@/hooks/Mapper/types';
|
||||
@@ -18,8 +20,6 @@ import {
|
||||
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
|
||||
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DetailedKill } from '../types/kills';
|
||||
import { ActivitySummary } from '../components/mapRootContent/components/CharacterActivity';
|
||||
import { TrackingCharacter } from '../components/mapRootContent/components/TrackAndFollow/types';
|
||||
|
||||
export type MapRootData = MapUnionTypes & {
|
||||
selectedSystems: string[];
|
||||
@@ -31,7 +31,6 @@ export type MapRootData = MapUnionTypes & {
|
||||
activity: ActivitySummary[];
|
||||
loading?: boolean;
|
||||
};
|
||||
showTrackAndFollow: boolean;
|
||||
trackingCharactersData: TrackingCharacter[];
|
||||
};
|
||||
|
||||
@@ -45,7 +44,6 @@ const INITIAL_DATA: MapRootData = {
|
||||
activity: [],
|
||||
loading: false,
|
||||
},
|
||||
showTrackAndFollow: false,
|
||||
trackingCharactersData: [],
|
||||
userCharacters: [],
|
||||
presentCharacters: [],
|
||||
@@ -62,6 +60,8 @@ const INITIAL_DATA: MapRootData = {
|
||||
options: {},
|
||||
isSubscriptionActive: false,
|
||||
linkSignatureToSystem: null,
|
||||
mainCharacterEveId: null,
|
||||
followingCharacterEveId: null,
|
||||
};
|
||||
|
||||
export enum AvailableThemes {
|
||||
@@ -166,6 +166,8 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
|
||||
},
|
||||
);
|
||||
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
|
||||
const comments = useComments({ outCommand });
|
||||
const charactersCache = useCharactersCache({ outCommand });
|
||||
|
||||
useEffect(() => {
|
||||
let foundNew = false;
|
||||
@@ -183,11 +185,9 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
|
||||
if (foundNew) {
|
||||
setInterfaceSettings(newVals);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const comments = useComments({ outCommand });
|
||||
const charactersCache = useCharactersCache({ outCommand });
|
||||
|
||||
return (
|
||||
<MapRootContext.Provider
|
||||
value={{
|
||||
|
||||
@@ -9,20 +9,25 @@ export const useMapInit = () => {
|
||||
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
|
||||
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
connections,
|
||||
effects,
|
||||
wormholes,
|
||||
system_static_infos,
|
||||
characters,
|
||||
user_characters,
|
||||
present_characters,
|
||||
hubs,
|
||||
user_permissions,
|
||||
options,
|
||||
is_subscription_active,
|
||||
}: CommandInit) => {
|
||||
(props: CommandInit) => {
|
||||
const {
|
||||
systems,
|
||||
system_signatures,
|
||||
connections,
|
||||
effects,
|
||||
wormholes,
|
||||
system_static_infos,
|
||||
characters,
|
||||
user_characters,
|
||||
present_characters,
|
||||
hubs,
|
||||
user_permissions,
|
||||
options,
|
||||
is_subscription_active,
|
||||
main_character_eve_id,
|
||||
following_character_eve_id,
|
||||
} = props;
|
||||
|
||||
const updateData: Partial<MapRootData> = {};
|
||||
|
||||
if (wormholes) {
|
||||
@@ -50,6 +55,10 @@ export const useMapInit = () => {
|
||||
updateData.systems = systems;
|
||||
}
|
||||
|
||||
if (system_signatures) {
|
||||
updateData.systemSignatures = system_signatures;
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
updateData.connections = connections;
|
||||
}
|
||||
@@ -66,7 +75,9 @@ export const useMapInit = () => {
|
||||
updateData.options = options;
|
||||
}
|
||||
|
||||
updateData.isSubscriptionActive = is_subscription_active;
|
||||
if (is_subscription_active) {
|
||||
updateData.isSubscriptionActive = is_subscription_active;
|
||||
}
|
||||
|
||||
if (system_static_infos) {
|
||||
system_static_infos.forEach(static_info => {
|
||||
@@ -74,6 +85,14 @@ export const useMapInit = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (main_character_eve_id) {
|
||||
updateData.mainCharacterEveId = main_character_eve_id;
|
||||
}
|
||||
|
||||
if ('following_character_eve_id' in props) {
|
||||
updateData.followingCharacterEveId = following_character_eve_id;
|
||||
}
|
||||
|
||||
update(updateData);
|
||||
},
|
||||
[update, addSystemStatic],
|
||||
|
||||
@@ -9,6 +9,13 @@ type SystemStaticResult = {
|
||||
// TODO maybe later we can store in Static data in provider
|
||||
const cache = new Map<number, SolarSystemStaticInfoRaw>();
|
||||
|
||||
export const getSystemStaticInfo = (solarSystemId: number | string | undefined) => {
|
||||
if (!solarSystemId) {
|
||||
return;
|
||||
}
|
||||
return cache.get(typeof solarSystemId == 'number' ? solarSystemId : parseInt(solarSystemId));
|
||||
};
|
||||
|
||||
export const loadSystemStaticInfo = async (outCommand: OutCommandHandler, systems: number[]) => {
|
||||
const result = await outCommand({
|
||||
type: OutCommand.getSystemStaticInfos,
|
||||
|
||||
@@ -57,112 +57,116 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const { addComment, removeComment } = useCommandComments();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export type ShipTypeRaw = {
|
||||
export type LocationRaw = {
|
||||
solar_system_id: number | null;
|
||||
structure_id: number | null;
|
||||
station_id: number | null;
|
||||
};
|
||||
|
||||
export type CharacterTypeRaw = {
|
||||
@@ -34,6 +35,11 @@ export type CharacterTypeRaw = {
|
||||
corporation_ticker: string;
|
||||
};
|
||||
|
||||
export interface TrackingCharacter {
|
||||
character: CharacterTypeRaw;
|
||||
tracked: boolean;
|
||||
}
|
||||
|
||||
export type WithIsOwnCharacter = {
|
||||
isOwn: boolean;
|
||||
};
|
||||
@@ -56,3 +62,10 @@ export interface UseCharactersCacheData {
|
||||
characters: Map<string, CharacterCache>;
|
||||
lastUpdateKey: number;
|
||||
}
|
||||
|
||||
export interface ActivitySummary {
|
||||
character: CharacterTypeRaw;
|
||||
passages: number;
|
||||
connections: number;
|
||||
signatures: number;
|
||||
}
|
||||
|
||||
7
assets/js/hooks/Mapper/types/commandsIn.ts
Normal file
7
assets/js/hooks/Mapper/types/commandsIn.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TrackingCharacter } from './character.ts';
|
||||
|
||||
export type CommandInCharactersTrackingInfo = {
|
||||
characters: TrackingCharacter[];
|
||||
following: string | null;
|
||||
main: string | null;
|
||||
};
|
||||
@@ -9,3 +9,5 @@ export interface WithClassName {
|
||||
}
|
||||
|
||||
export type WithHTMLProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export type IncomingEvent<T> = { data: T };
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types/system.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
|
||||
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { ActivitySummary } from '../components/mapRootContent/components/CharacterActivity';
|
||||
import { TrackingCharacter } from '../components/mapRootContent/components/TrackAndFollow/types';
|
||||
import { CommentType, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { CommentType, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
|
||||
|
||||
export enum Commands {
|
||||
init = 'init',
|
||||
@@ -37,6 +35,7 @@ export enum Commands {
|
||||
updateActivity = 'update_activity',
|
||||
updateTracking = 'update_tracking',
|
||||
userSettingsUpdated = 'user_settings_updated',
|
||||
showTracking = 'show_tracking',
|
||||
}
|
||||
|
||||
export type Command =
|
||||
@@ -62,14 +61,17 @@ export type Command =
|
||||
| Commands.signaturesUpdated
|
||||
| Commands.systemCommentAdded
|
||||
| Commands.systemCommentRemoved
|
||||
| Commands.systemCommentsUpdated
|
||||
| Commands.characterActivityData
|
||||
| Commands.trackingCharactersData
|
||||
| Commands.userSettingsUpdated
|
||||
| Commands.updateActivity
|
||||
| Commands.updateTracking;
|
||||
| Commands.updateTracking
|
||||
| Commands.showTracking;
|
||||
|
||||
export type CommandInit = {
|
||||
systems: SolarSystemRawType[];
|
||||
system_signatures: Record<string, SystemSignature[]>;
|
||||
kills: Kill[];
|
||||
system_static_infos: SolarSystemStaticInfoRaw[];
|
||||
connections: SolarSystemConnection[];
|
||||
@@ -84,6 +86,8 @@ export type CommandInit = {
|
||||
options: Record<string, string | boolean>;
|
||||
reset?: boolean;
|
||||
is_subscription_active?: boolean;
|
||||
main_character_eve_id?: string | null;
|
||||
following_character_eve_id?: string | null;
|
||||
};
|
||||
|
||||
export type CommandAddSystems = SolarSystemRawType[];
|
||||
@@ -123,14 +127,7 @@ export type CommandUserSettingsUpdated = {
|
||||
settings: UserSettings;
|
||||
};
|
||||
|
||||
export type CommandShowActivity = null;
|
||||
export type CommandHideActivity = null;
|
||||
export type CommandShowTracking = null;
|
||||
export type CommandHideTracking = null;
|
||||
export type CommandUiLoaded = { version: string | null };
|
||||
export type CommandLogMapError = { error: string; componentStack: string };
|
||||
export type CommandMapEvent = { type: Command; data: unknown };
|
||||
export type CommandMapEvents = Array<{ type: Command; data: unknown }>;
|
||||
export type CommandUpdateActivity = {
|
||||
characterId: number;
|
||||
systemId: number;
|
||||
@@ -186,6 +183,8 @@ export interface CommandData {
|
||||
[Commands.updateTracking]: CommandUpdateTracking;
|
||||
[Commands.systemCommentAdded]: CommandCommentAdd;
|
||||
[Commands.systemCommentRemoved]: CommandCommentRemoved;
|
||||
[Commands.systemCommentsUpdated]: unknown;
|
||||
[Commands.showTracking]: CommandShowTracking;
|
||||
}
|
||||
|
||||
export interface MapHandlers {
|
||||
@@ -201,6 +200,7 @@ export enum OutCommand {
|
||||
getSignatures = 'get_signatures',
|
||||
getSystemStaticInfos = 'get_system_static_infos',
|
||||
getConnectionInfo = 'get_connection_info',
|
||||
loadSignatures = 'load_signatures',
|
||||
updateConnectionTimeStatus = 'update_connection_time_status',
|
||||
updateConnectionType = 'update_connection_type',
|
||||
updateConnectionMassStatus = 'update_connection_mass_status',
|
||||
@@ -234,9 +234,13 @@ export enum OutCommand {
|
||||
addSystemComment = 'addSystemComment',
|
||||
deleteSystemComment = 'deleteSystemComment',
|
||||
getSystemComments = 'getSystemComments',
|
||||
toggleTrack = 'toggle_track',
|
||||
// toggleTrack = 'toggle_track',
|
||||
toggleFollow = 'toggle_follow',
|
||||
getCharacterInfo = 'getCharacterInfo',
|
||||
getCharactersTrackingInfo = 'getCharactersTrackingInfo',
|
||||
updateCharacterTracking = 'updateCharacterTracking',
|
||||
updateFollowingCharacter = 'updateFollowingCharacter',
|
||||
updateMainCharacter = 'updateMainCharacter',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
|
||||
@@ -23,4 +23,7 @@ export type MapUnionTypes = {
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
options: Record<string, string | boolean>;
|
||||
isSubscriptionActive: boolean;
|
||||
|
||||
mainCharacterEveId: string | null;
|
||||
followingCharacterEveId: string | null;
|
||||
};
|
||||
|
||||
BIN
assets/static/images/73_16_241.png
Normal file
BIN
assets/static/images/73_16_241.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 302 B |
BIN
assets/static/images/73_16_245.png
Normal file
BIN
assets/static/images/73_16_245.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 425 B |
BIN
assets/static/images/73_16_247.png
Normal file
BIN
assets/static/images/73_16_247.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user