mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-11 18:26:04 +00:00
Add hover tooltips for local counter and kills bookmark (#130)
* feat: add local pilots and kills display on hover
This commit is contained in:
@@ -125,10 +125,9 @@ const MapComp = ({
|
|||||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||||
const { update } = useMapState();
|
const { update } = useMapState();
|
||||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
const { variant, gap, size, color, snapSize } = useBackgroundVars(theme);
|
||||||
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
const { isPanAndDrag, nodeComponent, connectionMode } = getBehaviorForTheme(theme || 'default');
|
||||||
|
|
||||||
// You can create nodeTypes dynamically based on the node component
|
|
||||||
const nodeTypes = useMemo(() => {
|
const nodeTypes = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
custom: nodeComponent,
|
custom: nodeComponent,
|
||||||
@@ -256,7 +255,7 @@ const MapComp = ({
|
|||||||
|
|
||||||
onEdgesChange(nextChanges);
|
onEdgesChange(nextChanges);
|
||||||
},
|
},
|
||||||
[getEdge, getNode, onEdgesChange],
|
[canRemoveConnection, getEdge, getNode, onEdgesChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -283,6 +282,7 @@ const MapComp = ({
|
|||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
connectionMode={connectionMode}
|
connectionMode={connectionMode}
|
||||||
snapToGrid
|
snapToGrid
|
||||||
|
snapGrid={[snapSize, snapSize]}
|
||||||
nodeDragThreshold={10}
|
nodeDragThreshold={10}
|
||||||
onNodeDragStop={handleDragStop}
|
onNodeDragStop={handleDragStop}
|
||||||
onSelectionDragStop={handleSelectionDragStop}
|
onSelectionDragStop={handleSelectionDragStop}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
.KillsBookmark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
padding: 4px 3px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.KillsBookmarkWithIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: -2px;
|
||||||
|
text-shadow: 0 0 3px #000;
|
||||||
|
padding-right: 2px;
|
||||||
|
height: 8px;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
|
||||||
|
.pi {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 9px;
|
||||||
|
font-family: var(--rf-node-font-family, inherit) !important;
|
||||||
|
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipContainer {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
padding: 3px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: auto;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/SystemKillsContent/SystemKillsContent';
|
||||||
|
import { useKillsCounter } from '../../hooks/useKillsCounter';
|
||||||
|
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||||
|
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||||
|
|
||||||
|
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
type KillsBookmarkTooltipProps = {
|
||||||
|
killsCount: number;
|
||||||
|
killsActivityType: string | null;
|
||||||
|
systemId: string;
|
||||||
|
className?: string;
|
||||||
|
size?: TooltipSize;
|
||||||
|
} & WithChildren &
|
||||||
|
WithClassName;
|
||||||
|
|
||||||
|
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
|
||||||
|
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
|
||||||
|
|
||||||
|
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
|
||||||
|
|
||||||
|
const tooltipContent = (
|
||||||
|
<SystemKillsContent kills={detailedKills} systemNameMap={systemNameMap} compact={true} onlyOneSystem={true} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<WdTooltipWrapper content={tooltipContent} className={className} size={size} interactive={true}>
|
||||||
|
{children}
|
||||||
|
</WdTooltipWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||||
|
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||||
|
import { LocalCharactersList, CharItemProps } from '../../../mapInterface/widgets/LocalCharacters/components';
|
||||||
|
import { useLocalCharactersItemTemplate } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalCharacters';
|
||||||
|
import { useLocalCharacterWidgetSettings } from '../../../mapInterface/widgets/LocalCharacters/hooks/useLocalWidgetSettings';
|
||||||
|
|
||||||
|
interface LocalCounterProps {
|
||||||
|
localCounterCharacters: Array<CharItemProps>;
|
||||||
|
classes: { [key: string]: string };
|
||||||
|
hasUserCharacters: boolean;
|
||||||
|
showIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalCounter({
|
||||||
|
localCounterCharacters,
|
||||||
|
hasUserCharacters,
|
||||||
|
classes,
|
||||||
|
showIcon = true,
|
||||||
|
}: LocalCounterProps) {
|
||||||
|
const [settings] = useLocalCharacterWidgetSettings();
|
||||||
|
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||||
|
|
||||||
|
const pilotTooltipContent = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '300px',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
height: '300px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LocalCharactersList items={localCounterCharacters} itemTemplate={itemTemplate} itemSize={26} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [localCounterCharacters, itemTemplate]);
|
||||||
|
|
||||||
|
if (localCounterCharacters.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.LocalCounterLayer} style={{ zIndex: 9999 }}>
|
||||||
|
<WdTooltipWrapper
|
||||||
|
// @ts-ignore
|
||||||
|
content={pilotTooltipContent}
|
||||||
|
position={TooltipPosition.right}
|
||||||
|
offset={180}
|
||||||
|
interactive={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(classes.localCounter, {
|
||||||
|
[classes.hasUserCharacters]: hasUserCharacters,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{showIcon && <i className="pi pi-users" style={{ fontSize: '0.50rem' }} />}
|
||||||
|
<span>{localCounterCharacters.length}</span>
|
||||||
|
</div>
|
||||||
|
</WdTooltipWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,29 +2,25 @@
|
|||||||
|
|
||||||
$pastel-blue: #5a7d9a;
|
$pastel-blue: #5a7d9a;
|
||||||
$pastel-pink: #d291bc;
|
$pastel-pink: #d291bc;
|
||||||
$pastel-green: #88b04b;
|
|
||||||
$pastel-yellow: #ffdd59;
|
|
||||||
$dark-bg: #2d2d2d;
|
$dark-bg: #2d2d2d;
|
||||||
$text-color: #ffffff;
|
$text-color: #ffffff;
|
||||||
$tooltip-bg: #202020;
|
$tooltip-bg: #202020;
|
||||||
|
|
||||||
$node-bg-color: #202020;
|
|
||||||
$node-soft-bg-color: #202020;
|
|
||||||
$text-color: #ffffff;
|
|
||||||
$tag-color: #38BDF8;
|
|
||||||
$region-name: #D6D3D1;
|
|
||||||
$custom-name: #93C5FD;
|
|
||||||
|
|
||||||
.RootCustomNode {
|
.RootCustomNode {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 130px;
|
width: 130px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
|
||||||
|
font-family: var(--rf-node-font-family, inherit) !important;
|
||||||
|
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
||||||
background-color: $tooltip-bg;
|
background-color: var(--rf-node-bg-color, #202020) !important;
|
||||||
|
color: var(--rf-text-color, #ffffff);
|
||||||
|
|
||||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||||
border: 1px solid darken($pastel-blue, 10%);
|
border: 1px solid darken($pastel-blue, 10%);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -92,14 +88,6 @@ $custom-name: #93C5FD;
|
|||||||
box-shadow: 0 0 10px #9a1af1c2;
|
box-shadow: 0 0 10px #9a1af1c2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.tooltip {
|
|
||||||
background-color: $tooltip-bg;
|
|
||||||
color: $text-color;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid $pastel-pink;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.eve-system-status-home {
|
&.eve-system-status-home {
|
||||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
@@ -178,8 +166,6 @@ $custom-name: #93C5FD;
|
|||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
padding-right: 3px;
|
padding-right: 3px;
|
||||||
|
|
||||||
//background-color: #833ca4;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
@@ -266,26 +252,18 @@ $custom-name: #93C5FD;
|
|||||||
|
|
||||||
.TagTitle {
|
.TagTitle {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: medium;
|
font-weight: 500;
|
||||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||||
|
|
||||||
color: var(--rf-tag-color, #38BDF8);
|
color: var(--rf-tag-color, #38BDF8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox kostyl */
|
/* Firefox kostyl */
|
||||||
@-moz-document url-prefix() {
|
@-moz-document url-prefix() {
|
||||||
.classSystemName {
|
.classSystemName {
|
||||||
font-family: inherit !important;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.classSystemName {
|
|
||||||
//font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solarSystemName {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.BottomRow {
|
.BottomRow {
|
||||||
@@ -294,22 +272,23 @@ $custom-name: #93C5FD;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
|
|
||||||
.localCounter {
|
.hasLocalCounter {
|
||||||
display: flex;
|
margin-right: 1.25rem;
|
||||||
//align-items: center;
|
&.countAbove9 {
|
||||||
gap: 2px;
|
margin-right: 1.5rem;
|
||||||
|
|
||||||
& > i {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > span {
|
.lockIcon {
|
||||||
font-size: 9px;
|
font-size: 0.45rem;
|
||||||
line-height: 9px;
|
font-weight: bold;
|
||||||
font-weight: 500;
|
position: relative;
|
||||||
//margin-top: 1px;
|
}
|
||||||
}
|
|
||||||
|
.mapMarker {
|
||||||
|
font-size: 0.45rem;
|
||||||
|
font-weight: bold;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,3 +374,39 @@ $custom-name: #93C5FD;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.LocalCounterLayer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 8;
|
||||||
|
|
||||||
|
.localCounter {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: auto;
|
||||||
|
top: 10.5px;
|
||||||
|
right: 8px;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
gap: 2px;
|
||||||
|
color: var(--rf-node-local-counter, #5cb85c);
|
||||||
|
|
||||||
|
&.hasUserCharacters {
|
||||||
|
color: var(--rf-has-user-characters, #fbbf24);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > i {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 9px;
|
||||||
|
font-weight: var(--rf-local-counter-font-weight, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { MapSolarSystemType } from '../../map.types';
|
import { MapSolarSystemType } from '../../map.types';
|
||||||
import { Handle, Position, NodeProps } from 'reactflow';
|
import { Handle, NodeProps, Position } from 'reactflow';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import classes from './SolarSystemNodeDefault.module.scss';
|
import classes from './SolarSystemNodeDefault.module.scss';
|
||||||
import { PrimeIcons } from 'primereact/api';
|
import { PrimeIcons } from 'primereact/api';
|
||||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
import { useLocalCounter, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||||
import {
|
import {
|
||||||
|
EFFECT_BACKGROUND_STYLES,
|
||||||
MARKER_BOOKMARK_BG_STYLES,
|
MARKER_BOOKMARK_BG_STYLES,
|
||||||
STATUS_CLASSES,
|
STATUS_CLASSES,
|
||||||
EFFECT_BACKGROUND_STYLES,
|
|
||||||
} from '@/hooks/Mapper/components/map/constants';
|
} from '@/hooks/Mapper/components/map/constants';
|
||||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||||
|
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||||
|
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||||
|
|
||||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||||
const nodeVars = useSolarSystemNode(props);
|
const nodeVars = useSolarSystemNode(props);
|
||||||
|
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -22,7 +25,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
<div className={classes.Bookmarks}>
|
<div className={classes.Bookmarks}>
|
||||||
{nodeVars.labelCustom !== '' && (
|
{nodeVars.labelCustom !== '' && (
|
||||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -32,13 +35,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nodeVars.killsCount && (
|
{nodeVars.killsCount && nodeVars.killsCount > 0 && nodeVars.solarSystemId && (
|
||||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
<KillsCounter
|
||||||
|
killsCount={nodeVars.killsCount}
|
||||||
|
systemId={nodeVars.solarSystemId}
|
||||||
|
size="lg"
|
||||||
|
killsActivityType={nodeVars.killsActivityType}
|
||||||
|
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||||
|
>
|
||||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</KillsCounter>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nodeVars.labelsInfo.map(x => (
|
{nodeVars.labelsInfo.map(x => (
|
||||||
@@ -53,10 +62,8 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
classes.RootCustomNode,
|
classes.RootCustomNode,
|
||||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||||
classes[STATUS_CLASSES[nodeVars.status]],
|
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||||
{
|
{ [classes.selected]: nodeVars.selected },
|
||||||
[classes.selected]: nodeVars.selected,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{nodeVars.visible && (
|
{nodeVars.visible && (
|
||||||
@@ -88,7 +95,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
{nodeVars.isWormhole && (
|
{nodeVars.isWormhole && (
|
||||||
<div className={classes.statics}>
|
<div className={classes.statics}>
|
||||||
{nodeVars.sortedStatics.map(whClass => (
|
{nodeVars.sortedStatics.map(whClass => (
|
||||||
<WormholeClassComp key={whClass} id={whClass} />
|
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -114,24 +121,15 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="flex gap-1 items-center">
|
<div
|
||||||
{nodeVars.locked && (
|
className={clsx('flex items-center gap-1', {
|
||||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||||
)}
|
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||||
|
})}
|
||||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
>
|
||||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||||
)}
|
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||||
|
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||||
{nodeVars.charactersInSystem.length > 0 && (
|
|
||||||
<div
|
|
||||||
className={clsx(classes.localCounter, {
|
|
||||||
['text-amber-300']: nodeVars.hasUserCharacters,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
|
||||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +143,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
{nodeVars.unsplashedLeft.length > 0 && (
|
{nodeVars.unsplashedLeft.length > 0 && (
|
||||||
<div className={classes.Unsplashed}>
|
<div className={classes.Unsplashed}>
|
||||||
{nodeVars.unsplashedLeft.map(sig => (
|
{nodeVars.unsplashedLeft.map(sig => (
|
||||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -153,14 +151,14 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
{nodeVars.unsplashedRight.length > 0 && (
|
{nodeVars.unsplashedRight.length > 0 && (
|
||||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||||
{nodeVars.unsplashedRight.map(sig => (
|
{nodeVars.unsplashedRight.map(sig => (
|
||||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
className={clsx(classes.Handle, classes.HandleTop, {
|
className={clsx(classes.Handle, classes.HandleTop, {
|
||||||
@@ -202,6 +200,11 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
|||||||
id="d"
|
id="d"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<LocalCounter
|
||||||
|
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||||
|
localCounterCharacters={localCounterCharacters}
|
||||||
|
classes={classes}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,91 +1,6 @@
|
|||||||
@import './SolarSystemNodeDefault.module.scss';
|
@import './SolarSystemNodeDefault.module.scss';
|
||||||
|
|
||||||
/* ---------------------------
|
/* ---------------------------------------------
|
||||||
Only override what's different
|
Only override what's different from the base
|
||||||
--------------------------- */
|
Currently none required
|
||||||
|
---------------------------------------------- */
|
||||||
/* 1) .RootCustomNode:
|
|
||||||
- new background-color using CSS var
|
|
||||||
- plus color, font-family, and font-weight */
|
|
||||||
.RootCustomNode {
|
|
||||||
background-color: var(--rf-node-bg-color, #202020) !important;
|
|
||||||
color: var(--rf-text-color, #ffffff);
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 2) .Bookmarks:
|
|
||||||
- add var-based font family/weight
|
|
||||||
*/
|
|
||||||
.Bookmarks {
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 3) .HeadRow, .classTitle, .classSystemName:
|
|
||||||
- add new references to var-based font family/weight
|
|
||||||
*/
|
|
||||||
.HeadRow {
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
|
|
||||||
.classTitle {
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-moz-document url-prefix() {
|
|
||||||
.classSystemName {
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.classSystemName {
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 4) .BottomRow:
|
|
||||||
- introduces .tagTitle, .regionName, .customName, .localCounter
|
|
||||||
referencing new CSS variables */
|
|
||||||
.BottomRow {
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
|
|
||||||
.tagTitle {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: medium;
|
|
||||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
color: var(--rf-tag-color, #38BDF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.regionName {
|
|
||||||
color: var(--rf-region-name, #D6D3D1);
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customName {
|
|
||||||
color: var(--rf-custom-name, #93C5FD);
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.localCounter {
|
|
||||||
display: flex;
|
|
||||||
color: var(--rf-has-user-characters, #fbbf24);
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
gap: 2px;
|
|
||||||
|
|
||||||
.hasUserCharacters {
|
|
||||||
color: var(--rf-has-user-characters, #fbbf24);
|
|
||||||
font-family: var(--rf-node-font-family, inherit) !important;
|
|
||||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { MapSolarSystemType } from '../../map.types';
|
import { MapSolarSystemType } from '../../map.types';
|
||||||
import { Handle, Position, NodeProps } from 'reactflow';
|
import { Handle, NodeProps, Position } from 'reactflow';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import classes from './SolarSystemNodeTheme.module.scss';
|
import classes from './SolarSystemNodeTheme.module.scss';
|
||||||
import { PrimeIcons } from 'primereact/api';
|
import { PrimeIcons } from 'primereact/api';
|
||||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
import { useLocalCounter, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||||
import {
|
import {
|
||||||
|
EFFECT_BACKGROUND_STYLES,
|
||||||
MARKER_BOOKMARK_BG_STYLES,
|
MARKER_BOOKMARK_BG_STYLES,
|
||||||
STATUS_CLASSES,
|
STATUS_CLASSES,
|
||||||
EFFECT_BACKGROUND_STYLES,
|
|
||||||
} from '@/hooks/Mapper/components/map/constants';
|
} from '@/hooks/Mapper/components/map/constants';
|
||||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||||
|
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||||
|
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||||
|
|
||||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||||
const nodeVars = useSolarSystemNode(props);
|
const nodeVars = useSolarSystemNode(props);
|
||||||
|
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -32,13 +35,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nodeVars.killsCount && (
|
{nodeVars.killsCount && nodeVars.killsCount > 0 && nodeVars.solarSystemId && (
|
||||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
<KillsCounter
|
||||||
|
killsCount={nodeVars.killsCount}
|
||||||
|
systemId={nodeVars.solarSystemId}
|
||||||
|
size="lg"
|
||||||
|
killsActivityType={nodeVars.killsActivityType}
|
||||||
|
className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}
|
||||||
|
>
|
||||||
<div className={clsx(classes.BookmarkWithIcon)}>
|
<div className={clsx(classes.BookmarkWithIcon)}>
|
||||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</KillsCounter>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nodeVars.labelsInfo.map(x => (
|
{nodeVars.labelsInfo.map(x => (
|
||||||
@@ -53,10 +62,8 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
classes.RootCustomNode,
|
classes.RootCustomNode,
|
||||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||||
classes[STATUS_CLASSES[nodeVars.status]],
|
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||||
{
|
{ [classes.selected]: nodeVars.selected },
|
||||||
[classes.selected]: nodeVars.selected,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{nodeVars.visible && (
|
{nodeVars.visible && (
|
||||||
@@ -88,7 +95,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
{nodeVars.isWormhole && (
|
{nodeVars.isWormhole && (
|
||||||
<div className={classes.statics}>
|
<div className={classes.statics}>
|
||||||
{nodeVars.sortedStatics.map(whClass => (
|
{nodeVars.sortedStatics.map(whClass => (
|
||||||
<WormholeClassComp key={whClass} id={whClass} />
|
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -124,24 +131,15 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="flex gap-1 items-center">
|
<div
|
||||||
{nodeVars.locked && (
|
className={clsx('flex items-center gap-1', {
|
||||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||||
)}
|
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||||
|
})}
|
||||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
>
|
||||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||||
)}
|
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||||
|
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||||
{nodeVars.charactersInSystem.length > 0 && (
|
|
||||||
<div
|
|
||||||
className={clsx(classes.localCounter, {
|
|
||||||
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
|
||||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +153,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
{nodeVars.unsplashedLeft.length > 0 && (
|
{nodeVars.unsplashedLeft.length > 0 && (
|
||||||
<div className={classes.Unsplashed}>
|
<div className={classes.Unsplashed}>
|
||||||
{nodeVars.unsplashedLeft.map(sig => (
|
{nodeVars.unsplashedLeft.map(sig => (
|
||||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -163,14 +161,14 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
{nodeVars.unsplashedRight.length > 0 && (
|
{nodeVars.unsplashedRight.length > 0 && (
|
||||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||||
{nodeVars.unsplashedRight.map(sig => (
|
{nodeVars.unsplashedRight.map(sig => (
|
||||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||||
<Handle
|
<Handle
|
||||||
type="source"
|
type="source"
|
||||||
className={clsx(classes.Handle, classes.HandleTop, {
|
className={clsx(classes.Handle, classes.HandleTop, {
|
||||||
@@ -212,6 +210,11 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
|||||||
id="d"
|
id="d"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<LocalCounter
|
||||||
|
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||||
|
localCounterCharacters={localCounterCharacters}
|
||||||
|
classes={classes}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export function useBackgroundVars(themeName?: string) {
|
|||||||
const [gap, setGap] = useState<number>(16);
|
const [gap, setGap] = useState<number>(16);
|
||||||
const [size, setSize] = useState<number>(1);
|
const [size, setSize] = useState<number>(1);
|
||||||
const [color, setColor] = useState('#81818b');
|
const [color, setColor] = useState('#81818b');
|
||||||
|
const [snapSize, setSnapSize] = useState<number>(25);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// match any element whose entire `class` attribute ends with "-theme"
|
// match any element whose entire `class` attribute ends with "-theme"
|
||||||
@@ -29,16 +30,19 @@ export function useBackgroundVars(themeName?: string) {
|
|||||||
|
|
||||||
const cssVarGap = style.getPropertyValue('--rf-bg-gap');
|
const cssVarGap = style.getPropertyValue('--rf-bg-gap');
|
||||||
const cssVarSize = style.getPropertyValue('--rf-bg-size');
|
const cssVarSize = style.getPropertyValue('--rf-bg-size');
|
||||||
|
const cssVarSnapSize = style.getPropertyValue('--rf-snap-size');
|
||||||
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
|
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
|
||||||
|
|
||||||
const gapNum = parseInt(cssVarGap, 10) || 16;
|
const gapNum = parseInt(cssVarGap, 10) || 16;
|
||||||
const sizeNum = parseInt(cssVarSize, 10) || 1;
|
const sizeNum = parseInt(cssVarSize, 10) || 1;
|
||||||
|
const snapSize = parseInt(cssVarSnapSize, 10) || 25; //react-flow default
|
||||||
|
|
||||||
setVariant(finalVariant);
|
setVariant(finalVariant);
|
||||||
setGap(gapNum);
|
setGap(gapNum);
|
||||||
setSize(sizeNum);
|
setSize(sizeNum);
|
||||||
setColor(cssColor);
|
setColor(cssColor);
|
||||||
|
setSnapSize(snapSize);
|
||||||
}, [themeName]);
|
}, [themeName]);
|
||||||
|
|
||||||
return { variant, gap, size, color };
|
return { variant, gap, size, color, snapSize };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useSystemKills } from '../../mapInterface/widgets/SystemKills/hooks/useSystemKills';
|
||||||
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
|
|
||||||
|
interface UseKillsCounterProps {
|
||||||
|
realSystemId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
|
||||||
|
const { data: mapData, outCommand } = useMapRootState();
|
||||||
|
const { systems } = mapData;
|
||||||
|
|
||||||
|
const systemNameMap = useMemo(() => {
|
||||||
|
const m: Record<string, string> = {};
|
||||||
|
systems.forEach(sys => {
|
||||||
|
m[sys.id] = sys.temporary_name || sys.name || '???';
|
||||||
|
});
|
||||||
|
return m;
|
||||||
|
}, [systems]);
|
||||||
|
|
||||||
|
const { kills: allKills, isLoading } = useSystemKills({
|
||||||
|
systemId: realSystemId,
|
||||||
|
outCommand,
|
||||||
|
showAllVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredKills = useMemo(() => {
|
||||||
|
if (!allKills || allKills.length === 0) return [];
|
||||||
|
|
||||||
|
return [...allKills]
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||||
|
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||||
|
return bTime - aTime;
|
||||||
|
})
|
||||||
|
.slice(0, 10);
|
||||||
|
}, [allKills]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
kills: filteredKills,
|
||||||
|
systemNameMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -127,6 +127,10 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
|||||||
// do nothing here
|
// do nothing here
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case Commands.detailedKillsUpdated:
|
||||||
|
// do nothing here
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
|
|||||||
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
|
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager';
|
||||||
import { CharacterTypeRaw, OutCommand } from '@/hooks/Mapper/types';
|
import { CharacterTypeRaw, OutCommand, SystemSignature } from '@/hooks/Mapper/types';
|
||||||
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
||||||
|
|
||||||
function getActivityType(count: number) {
|
export type LabelInfo = {
|
||||||
|
id: string;
|
||||||
|
shortName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnsplashedSignatureType = SystemSignature & { sig_id: string };
|
||||||
|
|
||||||
|
function getActivityType(count: number): string {
|
||||||
if (count <= 5) return 'activityNormal';
|
if (count <= 5) return 'activityNormal';
|
||||||
if (count <= 30) return 'activityWarn';
|
if (count <= 30) return 'activityWarn';
|
||||||
return 'activityDanger';
|
return 'activityDanger';
|
||||||
@@ -26,12 +33,25 @@ const SpaceToClass: Record<string, string> = {
|
|||||||
[Spaces.Gallente]: 'Gallente',
|
[Spaces.Gallente]: 'Gallente',
|
||||||
};
|
};
|
||||||
|
|
||||||
function sortedLabels(labels: string[]) {
|
function sortedLabels(labels: string[]): LabelInfo[] {
|
||||||
if (!labels) return [];
|
if (!labels) return [];
|
||||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
|
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x] as LabelInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
export function useLocalCounter(nodeVars: SolarSystemNodeVars) {
|
||||||
|
const localCounterCharacters = useMemo(() => {
|
||||||
|
return nodeVars.charactersInSystem
|
||||||
|
.map(char => ({
|
||||||
|
...char,
|
||||||
|
compact: true,
|
||||||
|
isOwn: nodeVars.userCharacters.includes(char.eve_id),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}, [nodeVars.charactersInSystem, nodeVars.userCharacters]);
|
||||||
|
return { localCounterCharacters };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>): SolarSystemNodeVars {
|
||||||
const { id, data, selected } = props;
|
const { id, data, selected } = props;
|
||||||
const {
|
const {
|
||||||
system_static_info,
|
system_static_info,
|
||||||
@@ -71,7 +91,6 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
|||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
characters,
|
characters,
|
||||||
presentCharacters,
|
|
||||||
wormholesData,
|
wormholesData,
|
||||||
hubs,
|
hubs,
|
||||||
kills,
|
kills,
|
||||||
@@ -87,15 +106,14 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
|||||||
|
|
||||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||||
|
|
||||||
const systemSignatures = useMemo(
|
const systemSigs = useMemo(
|
||||||
() => mapSystemSignatures[solar_system_id] || system_signatures,
|
() => mapSystemSignatures[solar_system_id] || system_signatures,
|
||||||
[system_signatures, solar_system_id, mapSystemSignatures],
|
[system_signatures, solar_system_id, mapSystemSignatures],
|
||||||
);
|
);
|
||||||
|
|
||||||
const charactersInSystem = useMemo(() => {
|
const charactersInSystem = useMemo(() => {
|
||||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
|
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
|
||||||
// eslint-disable-next-line
|
}, [characters, solar_system_id]);
|
||||||
}, [characters, presentCharacters, solar_system_id]);
|
|
||||||
|
|
||||||
const isWormhole = isWormholeSpace(system_class);
|
const isWormhole = isWormholeSpace(system_class);
|
||||||
|
|
||||||
@@ -136,52 +154,65 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
|||||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||||
|
|
||||||
const temporaryName = useMemo(() => {
|
const computedTemporaryName = useMemo(() => {
|
||||||
if (!isTempSystemNameEnabled) {
|
if (!isTempSystemNameEnabled) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return temporary_name;
|
return temporary_name;
|
||||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
||||||
|
|
||||||
const systemName = useMemo(() => {
|
const systemName = useMemo(() => {
|
||||||
if (isTempSystemNameEnabled && temporaryName) {
|
if (isTempSystemNameEnabled && computedTemporaryName) {
|
||||||
return temporaryName;
|
return computedTemporaryName;
|
||||||
}
|
}
|
||||||
return solar_system_name;
|
return solar_system_name;
|
||||||
}, [isTempSystemNameEnabled, solar_system_name, temporaryName]);
|
}, [isTempSystemNameEnabled, solar_system_name, computedTemporaryName]);
|
||||||
|
|
||||||
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name) || null;
|
const customName = useMemo(() => {
|
||||||
|
if (isTempSystemNameEnabled && computedTemporaryName && name) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (solar_system_name !== name && name) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [isTempSystemNameEnabled, computedTemporaryName, name, solar_system_name]);
|
||||||
|
|
||||||
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
||||||
if (!isShowUnsplashedSignatures) {
|
if (!isShowUnsplashedSignatures) {
|
||||||
return [[], []];
|
return [[], []];
|
||||||
}
|
}
|
||||||
return prepareUnsplashedChunks(
|
return prepareUnsplashedChunks(
|
||||||
systemSignatures
|
systemSigs
|
||||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||||
.map(s => ({
|
.map(s => ({
|
||||||
eve_id: s.eve_id,
|
eve_id: s.eve_id,
|
||||||
type: s.type,
|
type: s.type,
|
||||||
custom_info: s.custom_info,
|
custom_info: s.custom_info,
|
||||||
})),
|
kind: s.kind,
|
||||||
|
name: s.name,
|
||||||
|
group: s.group,
|
||||||
|
sig_id: s.eve_id, // Add a unique key property
|
||||||
|
})) as UnsplashedSignatureType[],
|
||||||
);
|
);
|
||||||
}, [isShowUnsplashedSignatures, systemSignatures]);
|
}, [isShowUnsplashedSignatures, systemSigs]);
|
||||||
|
|
||||||
const nodeVars = {
|
// Ensure hubs are always strings.
|
||||||
|
const hubsAsStrings = useMemo(() => hubs.map(item => item.toString()), [hubs]);
|
||||||
|
|
||||||
|
const nodeVars: SolarSystemNodeVars = {
|
||||||
id,
|
id,
|
||||||
selected,
|
selected,
|
||||||
|
|
||||||
visible,
|
visible,
|
||||||
isWormhole,
|
isWormhole,
|
||||||
classTitleColor,
|
classTitleColor,
|
||||||
killsCount,
|
killsCount,
|
||||||
killsActivityType,
|
killsActivityType,
|
||||||
hasUserCharacters,
|
hasUserCharacters,
|
||||||
|
userCharacters,
|
||||||
showHandlers,
|
showHandlers,
|
||||||
regionClass,
|
regionClass,
|
||||||
systemName,
|
systemName,
|
||||||
@@ -195,10 +226,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
|||||||
sortedStatics,
|
sortedStatics,
|
||||||
effectName: effect_name,
|
effectName: effect_name,
|
||||||
regionName: region_name,
|
regionName: region_name,
|
||||||
solarSystemId: solar_system_id,
|
solarSystemId: solar_system_id.toString(),
|
||||||
solarSystemName: solar_system_name,
|
solarSystemName: solar_system_name,
|
||||||
locked,
|
locked,
|
||||||
hubs,
|
hubs: hubsAsStrings,
|
||||||
name: name,
|
name: name,
|
||||||
isConnecting,
|
isConnecting,
|
||||||
hoverNodeId,
|
hoverNodeId,
|
||||||
@@ -207,7 +238,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
|||||||
unsplashedRight,
|
unsplashedRight,
|
||||||
isThickConnections,
|
isThickConnections,
|
||||||
classTitle: class_title,
|
classTitle: class_title,
|
||||||
temporaryName: temporary_name,
|
temporaryName: computedTemporaryName,
|
||||||
};
|
};
|
||||||
|
|
||||||
return nodeVars;
|
return nodeVars;
|
||||||
@@ -230,24 +261,22 @@ export interface SolarSystemNodeVars {
|
|||||||
isShattered: boolean;
|
isShattered: boolean;
|
||||||
tag?: string | null;
|
tag?: string | null;
|
||||||
status?: number;
|
status?: number;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
labelsInfo: LabelInfo[];
|
||||||
labelsInfo: Array<any>;
|
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
dbClick: (event?: void) => void;
|
|
||||||
sortedStatics: Array<string | number>;
|
sortedStatics: Array<string | number>;
|
||||||
effectName: string | null;
|
effectName: string | null;
|
||||||
regionName: string | null;
|
regionName: string | null;
|
||||||
solarSystemId: number;
|
solarSystemId: string;
|
||||||
solarSystemName: string | null;
|
solarSystemName: string | null;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
hubs: string[] | number[];
|
hubs: string[];
|
||||||
name: string | null;
|
name: string | null;
|
||||||
isConnecting: boolean;
|
isConnecting: boolean;
|
||||||
hoverNodeId: string | null;
|
hoverNodeId: string | null;
|
||||||
charactersInSystem: Array<CharacterTypeRaw>;
|
charactersInSystem: Array<CharacterTypeRaw>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
userCharacters: string[];
|
||||||
unsplashedLeft: Array<any>;
|
unsplashedLeft: Array<SystemSignature>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
unsplashedRight: Array<SystemSignature>;
|
||||||
unsplashedRight: Array<any>;
|
|
||||||
isThickConnections: boolean;
|
isThickConnections: boolean;
|
||||||
classTitle: string | null;
|
classTitle: string | null;
|
||||||
temporaryName?: string | null;
|
temporaryName?: string | null;
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
--rf-tag-color: #38BDF8;
|
--rf-tag-color: #38BDF8;
|
||||||
--rf-region-name: #D6D3D1;
|
--rf-region-name: #D6D3D1;
|
||||||
--rf-custom-name: #93C5FD;
|
--rf-custom-name: #93C5FD;
|
||||||
|
--rf-node-font-family: 'Shentox', 'Rogan', sans-serif !important;
|
||||||
|
--rf-node-font-weight: 500;
|
||||||
|
|
||||||
--rf-bg-variant: "dots";
|
--rf-bg-variant: "dots";
|
||||||
--rf-bg-gap: 15;
|
--rf-bg-gap: 15;
|
||||||
@@ -28,4 +29,8 @@
|
|||||||
--tooltip-bg: #202020;
|
--tooltip-bg: #202020;
|
||||||
|
|
||||||
--window-corner: #72716f;
|
--window-corner: #72716f;
|
||||||
|
|
||||||
|
--rf-local-counter-font-weight: 500;
|
||||||
|
--rf-node-local-counter: #5cb85c;
|
||||||
|
--rf-has-user-characters: #fbbf24;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,26 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||||
|
|
||||||
.pathfinder-theme {
|
.pathfinder-theme {
|
||||||
|
/* -- Override values from the default theme -- */
|
||||||
--rf-bg-color: #000000;
|
--rf-bg-color: #000000;
|
||||||
--rf-soft-bg-color: #282828;
|
--rf-soft-bg-color: #282828;
|
||||||
|
|
||||||
--rf-node-bg-color: #202020;
|
|
||||||
--rf-node-soft-bg-color: #313335;
|
--rf-node-soft-bg-color: #313335;
|
||||||
--rf-node-font-weight: bold;
|
--rf-node-font-weight: bold;
|
||||||
--rf-text-color: #adadad;
|
--rf-text-color: #adadad;
|
||||||
--rf-region-name: var(--rf-text-color);
|
--rf-region-name: var(--rf-text-color);
|
||||||
--rf-custom-name: var(--rf-text-color);
|
--rf-custom-name: var(--rf-text-color);
|
||||||
|
|
||||||
--tooltip-bg: #202020;
|
|
||||||
|
|
||||||
--rf-bg-variant: "lines";
|
--rf-bg-variant: "lines";
|
||||||
--rf-bg-gap: 32;
|
--rf-bg-gap: 34;
|
||||||
--rf-bg-size: 1;
|
--rf-snap-size: 17;
|
||||||
--rf-bg-pattern-color: #313131;
|
--rf-bg-pattern-color: #313131;
|
||||||
|
--rf-local-counter-font-weight: 700;
|
||||||
|
|
||||||
|
/* Additional node-specific overrides */
|
||||||
|
--rf-node-line-height: normal;
|
||||||
|
--rf-node-font-family: 'Oxygen', sans-serif;
|
||||||
|
--rf-tag-color: #fbbf24;
|
||||||
|
|
||||||
|
/* -- theme-specific variables -- */
|
||||||
--eve-effect-pulsar: #428bca;
|
--eve-effect-pulsar: #428bca;
|
||||||
--eve-effect-magnetar: #e06fdf;
|
--eve-effect-magnetar: #e06fdf;
|
||||||
--eve-effect-wolfRayet: #e28a0d;
|
--eve-effect-wolfRayet: #e28a0d;
|
||||||
@@ -38,14 +41,4 @@
|
|||||||
--eve-wh-type-color-c6: #d9534f;
|
--eve-wh-type-color-c6: #d9534f;
|
||||||
--eve-wh-type-color-c13: #7986cb;
|
--eve-wh-type-color-c13: #7986cb;
|
||||||
--eve-wh-type-color-drifter: #44aa82;
|
--eve-wh-type-color-drifter: #44aa82;
|
||||||
|
|
||||||
--rf-node-font-weight: bold;
|
|
||||||
--rf-node-line-height: normal;
|
|
||||||
--rf-node-font-family: 'Oxygen', sans-serif;
|
|
||||||
--rf-node-text-color: var(--pf-text-color);
|
|
||||||
|
|
||||||
--rf-tag-color: #fbbf24;
|
|
||||||
--rf-has-user-characters: #5cb85c;
|
|
||||||
|
|
||||||
--window-corner: #72716f;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,25 @@
|
|||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import classes from './LocalCharacters.module.scss';
|
import { LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
|
||||||
import { CharacterCard, LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
|
||||||
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
|
import { sortCharacters } from '@/hooks/Mapper/components/mapInterface/helpers/sortCharacters.ts';
|
||||||
import useLocalStorageState from 'use-local-storage-state';
|
|
||||||
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
import { useMapCheckPermissions, useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||||
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||||
|
import { LocalCharactersList } from './components/LocalCharactersList';
|
||||||
type CharItemProps = {
|
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
|
||||||
compact: boolean;
|
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
|
||||||
} & CharacterTypeRaw &
|
|
||||||
WithIsOwnCharacter;
|
|
||||||
|
|
||||||
const useItemTemplate = () => {
|
|
||||||
const {
|
|
||||||
data: { presentCharacters },
|
|
||||||
} = useMapRootState();
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(classes.CharacterRow, 'w-full box-border', {
|
|
||||||
'surface-hover': options.odd,
|
|
||||||
['border-b border-gray-600 border-opacity-20']: !options.last,
|
|
||||||
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: char.online,
|
|
||||||
})}
|
|
||||||
style={{ height: options.props.itemSize + 'px' }}
|
|
||||||
>
|
|
||||||
<CharacterCard showShipName {...char} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line
|
|
||||||
[presentCharacters],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type WindowLocalSettingsType = {
|
|
||||||
compact: boolean;
|
|
||||||
showOffline: boolean;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const STORED_DEFAULT_VALUES: WindowLocalSettingsType = {
|
|
||||||
compact: true,
|
|
||||||
showOffline: false,
|
|
||||||
version: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LocalCharacters = () => {
|
export const LocalCharacters = () => {
|
||||||
const {
|
const {
|
||||||
data: { characters, userCharacters, selectedSystems, presentCharacters },
|
data: { characters, userCharacters, selectedSystems },
|
||||||
} = useMapRootState();
|
} = useMapRootState();
|
||||||
|
|
||||||
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:local:settings', {
|
const [settings, setSettings] = useLocalCharacterWidgetSettings();
|
||||||
defaultValue: STORED_DEFAULT_VALUES,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [systemId] = selectedSystems;
|
const [systemId] = selectedSystems;
|
||||||
|
|
||||||
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
||||||
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
||||||
|
|
||||||
@@ -74,21 +28,31 @@ export const LocalCharacters = () => {
|
|||||||
[isAdminOrManager, restrictOfflineShowing],
|
[isAdminOrManager, restrictOfflineShowing],
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemTemplate = useItemTemplate();
|
|
||||||
|
|
||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
const sorted = characters
|
const filtered = characters
|
||||||
.filter(x => x.location?.solar_system_id?.toString() === systemId)
|
.filter(x => x.location?.solar_system_id?.toString() === systemId)
|
||||||
.map(x => ({ ...x, isOwn: userCharacters.includes(x.eve_id), compact: settings.compact }))
|
.map(x => ({
|
||||||
|
...x,
|
||||||
|
isOwn: userCharacters.includes(x.eve_id),
|
||||||
|
compact: settings.compact,
|
||||||
|
showShipName: settings.showShipName,
|
||||||
|
}))
|
||||||
.sort(sortCharacters);
|
.sort(sortCharacters);
|
||||||
|
|
||||||
if (!showOffline || !settings.showOffline) {
|
if (!showOffline || !settings.showOffline) {
|
||||||
return sorted.filter(c => c.online);
|
return filtered.filter(c => c.online);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sorted;
|
return filtered;
|
||||||
// eslint-disable-next-line
|
}, [
|
||||||
}, [showOffline, characters, settings.showOffline, settings.compact, systemId, userCharacters, presentCharacters]);
|
characters,
|
||||||
|
systemId,
|
||||||
|
userCharacters,
|
||||||
|
settings.compact,
|
||||||
|
settings.showOffline,
|
||||||
|
settings.showShipName,
|
||||||
|
showOffline,
|
||||||
|
]);
|
||||||
|
|
||||||
const isNobodyHere = sorted.length === 0;
|
const isNobodyHere = sorted.length === 0;
|
||||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||||
@@ -97,6 +61,8 @@ export const LocalCharacters = () => {
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const compact = useMaxWidth(ref, 145);
|
const compact = useMaxWidth(ref, 145);
|
||||||
|
|
||||||
|
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget
|
<Widget
|
||||||
label={
|
label={
|
||||||
@@ -111,7 +77,20 @@ export const LocalCharacters = () => {
|
|||||||
label={compact ? '' : 'Show offline'}
|
label={compact ? '' : 'Show offline'}
|
||||||
value={settings.showOffline}
|
value={settings.showOffline}
|
||||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||||
onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))}
|
onChange={() => setSettings(prev => ({ ...prev, showOffline: !prev.showOffline }))}
|
||||||
|
/>
|
||||||
|
</WdTooltipWrapper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settings.compact && (
|
||||||
|
<WdTooltipWrapper content="Show ship name in compact rows">
|
||||||
|
<WdCheckbox
|
||||||
|
size="xs"
|
||||||
|
labelSide="left"
|
||||||
|
label={compact ? '' : 'Show ship name'}
|
||||||
|
value={settings.showShipName}
|
||||||
|
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||||
|
onChange={() => setSettings(prev => ({ ...prev, showShipName: !prev.showShipName }))}
|
||||||
/>
|
/>
|
||||||
</WdTooltipWrapper>
|
</WdTooltipWrapper>
|
||||||
)}
|
)}
|
||||||
@@ -121,8 +100,8 @@ export const LocalCharacters = () => {
|
|||||||
['hero-bars-2']: settings.compact,
|
['hero-bars-2']: settings.compact,
|
||||||
['hero-bars-3']: !settings.compact,
|
['hero-bars-3']: !settings.compact,
|
||||||
})}
|
})}
|
||||||
onClick={() => setSettings(() => ({ ...settings, compact: !settings.compact }))}
|
onClick={() => setSettings(prev => ({ ...prev, compact: !prev.compact }))}
|
||||||
></span>
|
/>
|
||||||
</LayoutEventBlocker>
|
</LayoutEventBlocker>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -140,15 +119,11 @@ export const LocalCharacters = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showList && (
|
{showList && (
|
||||||
<VirtualScroller
|
<LocalCharactersList
|
||||||
items={sorted}
|
items={sorted}
|
||||||
itemSize={settings.compact ? 26 : 41}
|
itemSize={settings.compact ? 26 : 41}
|
||||||
itemTemplate={itemTemplate}
|
itemTemplate={itemTemplate}
|
||||||
className={clsx(
|
containerClassName="w-full h-full overflow-x-hidden overflow-y-auto"
|
||||||
classes.VirtualScroller,
|
|
||||||
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
|
|
||||||
)}
|
|
||||||
autoSize={false}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// .VirtualScroller {
|
||||||
|
// height: 100% !important;
|
||||||
|
// }
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { CharItemProps } from './types';
|
||||||
|
|
||||||
|
type LocalCharactersListProps = {
|
||||||
|
items: Array<CharItemProps>;
|
||||||
|
|
||||||
|
itemSize: number;
|
||||||
|
|
||||||
|
itemTemplate: (char: CharItemProps, options: VirtualScrollerTemplateOptions) => React.ReactNode;
|
||||||
|
|
||||||
|
containerClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LocalCharactersList({ items, itemSize, itemTemplate, containerClassName }: LocalCharactersListProps) {
|
||||||
|
return (
|
||||||
|
<VirtualScroller
|
||||||
|
items={items}
|
||||||
|
itemSize={itemSize}
|
||||||
|
orientation="vertical"
|
||||||
|
className={clsx('w-full h-full', containerClassName)}
|
||||||
|
autoSize={false}
|
||||||
|
itemTemplate={itemTemplate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './LocalCharactersList';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||||
|
|
||||||
|
export type CharItemProps = {
|
||||||
|
compact: boolean;
|
||||||
|
} & CharacterTypeRaw &
|
||||||
|
WithIsOwnCharacter;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import classes from './useLocalCharacters.module.scss';
|
||||||
|
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
|
||||||
|
import { CharItemProps } from '../components';
|
||||||
|
|
||||||
|
export function useLocalCharactersItemTemplate(showShipName: boolean) {
|
||||||
|
return useCallback(
|
||||||
|
(char: CharItemProps, options: VirtualScrollerTemplateOptions) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(classes.CharacterRow, 'box-border flex items-center', {
|
||||||
|
'surface-hover': options.odd,
|
||||||
|
'border-b border-gray-600 border-opacity-20': !options.last,
|
||||||
|
'bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10': char.online,
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
height: `${options.props.itemSize}px`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
minWidth: 0,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CharacterCard showShipName={showShipName} {...char} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[showShipName],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import useLocalStorageState from 'use-local-storage-state';
|
||||||
|
|
||||||
|
export interface LocalCharacterWidgetSettings {
|
||||||
|
compact: boolean;
|
||||||
|
showOffline: boolean;
|
||||||
|
version: number;
|
||||||
|
showShipName: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LOCAL_CHARACTER_WIDGET_DEFAULT: LocalCharacterWidgetSettings = {
|
||||||
|
compact: true,
|
||||||
|
showOffline: false,
|
||||||
|
version: 0,
|
||||||
|
showShipName: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useLocalCharacterWidgetSettings() {
|
||||||
|
return useLocalStorageState<LocalCharacterWidgetSettings>('kills:widget:settings', {
|
||||||
|
defaultValue: LOCAL_CHARACTER_WIDGET_DEFAULT,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ export interface KillsWidgetSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
||||||
compact: false,
|
compact: true,
|
||||||
showAll: false,
|
showAll: false,
|
||||||
excludedSystems: [],
|
excludedSystems: [],
|
||||||
version: 0,
|
version: 0,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
.CharIcon {
|
.CharIcon {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
border: 1px solid #2b2b2b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CharRow {
|
.CharRow {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { emitMapEvent } from '@/hooks/Mapper/events';
|
|||||||
|
|
||||||
type CharacterCardProps = {
|
type CharacterCardProps = {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
showShipName?: boolean;
|
|
||||||
showSystem?: boolean;
|
showSystem?: boolean;
|
||||||
|
showShipName?: boolean;
|
||||||
useSystemsCache?: boolean;
|
useSystemsCache?: boolean;
|
||||||
} & CharacterTypeRaw &
|
} & CharacterTypeRaw &
|
||||||
WithIsOwnCharacter;
|
WithIsOwnCharacter;
|
||||||
@@ -22,8 +22,15 @@ export const getShipName = (name: string) => {
|
|||||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => String.fromCharCode(parseInt(grp, 16)));
|
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => String.fromCharCode(parseInt(grp, 16)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A small divider between fields:
|
||||||
|
const Divider = () => (
|
||||||
|
<span className="mx-1 text-gray-400" aria-hidden="true">
|
||||||
|
|
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
export const CharacterCard = ({
|
export const CharacterCard = ({
|
||||||
compact,
|
compact = false,
|
||||||
isOwn,
|
isOwn,
|
||||||
showSystem,
|
showSystem,
|
||||||
showShipName,
|
showShipName,
|
||||||
@@ -37,59 +44,135 @@ export const CharacterCard = ({
|
|||||||
});
|
});
|
||||||
}, [char]);
|
}, [char]);
|
||||||
|
|
||||||
|
// Precompute the ship name (decoded):
|
||||||
|
const shipNameText = char.ship?.ship_name ? getShipName(char.ship.ship_name) : '';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// COMPACT MODE: Main line =
|
||||||
|
// if (showShipName & haveShipName) => name | shipName (skip ticker)
|
||||||
|
// else => name | [ticker]
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const compactLine = (
|
||||||
|
<>
|
||||||
|
{/* Character Name (lighter shade) */}
|
||||||
|
<span className="text-gray-200">{char.name}</span>
|
||||||
|
<Divider />
|
||||||
|
{showShipName && shipNameText ? (
|
||||||
|
// Show the ship name in place of the ticker (use indigo color to match corp/alliance)
|
||||||
|
<span className="text-indigo-300">{shipNameText}</span>
|
||||||
|
) : (
|
||||||
|
// Show the [ticker] (indigo)
|
||||||
|
<span className="text-indigo-300">[{char.alliance_id ? char.alliance_ticker : char.corporation_ticker}]</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// NON-COMPACT MODE:
|
||||||
|
// Line 1 => name | [ticker]
|
||||||
|
// Line 2 => (shipName) always, if it exists
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
const nonCompactLine1 = (
|
||||||
|
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{/* Character Name (lighter shade) */}
|
||||||
|
<span className="text-gray-200">{char.name}</span>
|
||||||
|
<Divider />
|
||||||
|
<span className="text-indigo-300">[{char.alliance_id ? char.alliance_ticker : char.corporation_ticker}]</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nonCompactLine2 = (
|
||||||
|
<>
|
||||||
|
{shipNameText && (
|
||||||
|
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-gray-300">{shipNameText}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')} onClick={handleSelect}>
|
<div className={clsx(classes.CharacterCard, 'w-full text-xs box-border')} onClick={handleSelect}>
|
||||||
<div className="flex px-2 py-1 gap-1">
|
<div
|
||||||
{!compact && (
|
className={clsx(
|
||||||
|
'w-full px-2 py-1 overflow-hidden gap-1',
|
||||||
|
compact ? 'grid items-center' : 'flex flex-col md:flex-row items-start',
|
||||||
|
)}
|
||||||
|
style={compact ? { gridTemplateColumns: 'auto 1fr auto', minWidth: 0 } : undefined}
|
||||||
|
>
|
||||||
|
{compact ? (
|
||||||
|
<img
|
||||||
|
src={`https://images.evetech.net/characters/${char.eve_id}/portrait`}
|
||||||
|
alt={`${char.name} portrait`}
|
||||||
|
style={{
|
||||||
|
width: '18px',
|
||||||
|
height: '18px',
|
||||||
|
// Remove circle shape for a square image:
|
||||||
|
borderRadius: 0,
|
||||||
|
marginRight: '4px',
|
||||||
|
flexShrink: 0,
|
||||||
|
// Slightly lighter than typical dark background:
|
||||||
|
border: '1px solid #2b2b2b',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<span
|
<span
|
||||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||||
style={{
|
style={{
|
||||||
|
// The SCSS below ensures the image is square with a border.
|
||||||
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
|
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col flex-grow">
|
|
||||||
|
{/*
|
||||||
|
Middle section:
|
||||||
|
- In compact mode, everything is on one line (Name + possibly ShipName or ticker).
|
||||||
|
- In non-compact mode, line 1 has (Name | Ticker), line 2 has shipName if it exists.
|
||||||
|
*/}
|
||||||
|
<div
|
||||||
|
className={clsx('overflow-hidden text-ellipsis', {
|
||||||
|
'text-left px-1': compact,
|
||||||
|
'flex-grow': !compact,
|
||||||
|
})}
|
||||||
|
style={{ minWidth: 0 }}
|
||||||
|
>
|
||||||
|
{/* This left border highlights "isOwn" in the same way as older code. */}
|
||||||
<div
|
<div
|
||||||
className={clsx(classes.CharRow, 'w-full', {
|
className={clsx('overflow-hidden whitespace-nowrap', {
|
||||||
[classes.TwoColumns]: !char.ship,
|
[classes.CardBorderLeftIsOwn]: isOwn,
|
||||||
[classes.ThreeColumns]: char.ship,
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span
|
{compact ? compactLine : nonCompactLine1}
|
||||||
className={clsx(classes.CharName, 'text-ellipsis overflow-hidden whitespace-nowrap', {
|
|
||||||
[classes.CardBorderLeftIsOwn]: isOwn,
|
|
||||||
})}
|
|
||||||
title={char.name}
|
|
||||||
>
|
|
||||||
{char.name}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{char.alliance_id && <span className="text-gray-400">[{char.alliance_ticker}]</span>}
|
|
||||||
{!char.alliance_id && <span className="text-gray-400">[{char.corporation_ticker}]</span>}
|
|
||||||
|
|
||||||
{char.ship?.ship_type_info && (
|
|
||||||
<div
|
|
||||||
className="flex-grow text-ellipsis overflow-hidden whitespace-nowrap"
|
|
||||||
title={char.ship.ship_type_info.name}
|
|
||||||
>
|
|
||||||
{char.ship.ship_type_info.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* Non-compact second line always shows shipName if available */}
|
||||||
{showShipName && !compact && char.ship?.ship_name && (
|
{!compact && nonCompactLine2}
|
||||||
<div className="grid w-full">
|
|
||||||
<span className="text-ellipsis overflow-hidden whitespace-nowrap">
|
|
||||||
{getShipName(char.ship.ship_name)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showSystem && !compact && char.location?.solar_system_id && (
|
|
||||||
<SystemView systemId={char.location.solar_system_id.toString()} useSystemsCache={useSystemsCache} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Right column for Ship Type (compact) or "pushed" to the right (non-compact).
|
||||||
|
Ship Type remains text-yellow-400.
|
||||||
|
*/}
|
||||||
|
{char.ship?.ship_type_info?.name && (
|
||||||
|
<div
|
||||||
|
className={clsx('text-yellow-400 text-ellipsis overflow-hidden whitespace-nowrap', {
|
||||||
|
'text-right px-1 flex-shrink-0': compact,
|
||||||
|
'mt-1 md:mt-0 ml-auto': !compact,
|
||||||
|
})}
|
||||||
|
style={{ maxWidth: compact ? '120px' : '200px' }}
|
||||||
|
title={char.ship.ship_type_info.name}
|
||||||
|
>
|
||||||
|
{char.ship.ship_type_info.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
System row at the bottom if `showSystem && system exists`.
|
||||||
|
*/}
|
||||||
|
{showSystem && char.location?.solar_system_id && (
|
||||||
|
<div className="px-2 pb-1">
|
||||||
|
<SystemView systemId={char.location.solar_system_id.toString()} useSystemsCache={useSystemsCache} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export enum TooltipPosition {
|
|||||||
bottom = 'bottom',
|
bottom = 'bottom',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
|
||||||
position?: TooltipPosition;
|
position?: TooltipPosition;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
content: (() => React.ReactNode) | React.ReactNode;
|
content: (() => React.ReactNode) | React.ReactNode;
|
||||||
@@ -27,183 +27,253 @@ export interface OffsetPosition {
|
|||||||
|
|
||||||
export interface WdTooltipHandlers {
|
export interface WdTooltipHandlers {
|
||||||
show: (e?: React.MouseEvent) => void;
|
show: (e?: React.MouseEvent) => void;
|
||||||
hide: (e?: React.MouseEvent) => void;
|
hide: () => void;
|
||||||
getIsMouseInside: () => boolean;
|
getIsMouseInside: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WdTooltip = forwardRef(
|
const LEAVE_DELAY = 100;
|
||||||
(props: TooltipProps & { className?: string }, ref: ForwardedRef<WdTooltipHandlers>) => {
|
|
||||||
const {
|
|
||||||
content,
|
|
||||||
targetSelector,
|
|
||||||
position: tPosition = TooltipPosition.default,
|
|
||||||
className,
|
|
||||||
offset = 5,
|
|
||||||
interactive = false,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
export const WdTooltip = forwardRef(function WdTooltip(
|
||||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
{
|
||||||
const [ev, setEv] = useState<React.MouseEvent>();
|
content,
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
targetSelector,
|
||||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
position: tPosition = TooltipPosition.default,
|
||||||
|
offset = 5,
|
||||||
|
interactive = false,
|
||||||
|
className,
|
||||||
|
...restProps
|
||||||
|
}: TooltipProps,
|
||||||
|
ref: ForwardedRef<WdTooltipHandlers>,
|
||||||
|
) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||||
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||||
if (!tooltipRef.current) return { left: x, top: y };
|
|
||||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
|
||||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
|
||||||
let newLeft = x;
|
|
||||||
let newTop = y;
|
|
||||||
|
|
||||||
if (newLeft < 0) newLeft = 10;
|
const [reactEvt, setReactEvt] = useState<React.MouseEvent>();
|
||||||
if (newTop < 0) newTop = 10;
|
|
||||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
|
||||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
|
||||||
}
|
|
||||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
|
||||||
newTop = window.innerHeight - tooltipHeight - 10;
|
|
||||||
}
|
|
||||||
return { left: newLeft, top: newTop };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
show: (mouseEvt?: React.MouseEvent) => {
|
|
||||||
if (mouseEvt) setEv(mouseEvt);
|
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||||
setPos(null);
|
if (!tooltipRef.current) return { left: x, top: y };
|
||||||
setVisible(true);
|
|
||||||
},
|
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||||
hide: () => {
|
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||||
|
|
||||||
|
let newLeft = x;
|
||||||
|
let newTop = y;
|
||||||
|
|
||||||
|
if (newLeft < 0) newLeft = 10;
|
||||||
|
if (newTop < 0) newTop = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { left: newLeft, top: newTop };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scheduleHide = useCallback(() => {
|
||||||
|
if (!interactive) {
|
||||||
|
setVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hideTimeoutRef.current) {
|
||||||
|
hideTimeoutRef.current = setTimeout(() => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
},
|
}, LEAVE_DELAY);
|
||||||
getIsMouseInside: () => isMouseInsideTooltip,
|
}
|
||||||
}));
|
}, [interactive]);
|
||||||
|
|
||||||
useEffect(() => {
|
useImperativeHandle(ref, () => ({
|
||||||
if (!tooltipRef.current || !ev) return;
|
show: (e?: React.MouseEvent) => {
|
||||||
const tooltipEl = tooltipRef.current;
|
if (hideTimeoutRef.current) {
|
||||||
const { clientX, clientY, target } = ev;
|
clearTimeout(hideTimeoutRef.current);
|
||||||
const targetBounds = (target as HTMLElement).getBoundingClientRect();
|
hideTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
if (e && tooltipRef.current) {
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
setPos(calcTooltipPosition({ x: clientX, y: clientY }));
|
||||||
|
setReactEvt(e);
|
||||||
|
}
|
||||||
|
setVisible(true);
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
if (hideTimeoutRef.current) {
|
||||||
|
clearTimeout(hideTimeoutRef.current);
|
||||||
|
}
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
getIsMouseInside: () => isMouseInsideTooltip,
|
||||||
|
}));
|
||||||
|
|
||||||
let offsetX = clientX;
|
useEffect(() => {
|
||||||
let offsetY = clientY;
|
if (!tooltipRef.current || !reactEvt) return;
|
||||||
|
|
||||||
if (tPosition === TooltipPosition.left) {
|
const { clientX, clientY, target } = reactEvt;
|
||||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
const tooltipEl = tooltipRef.current;
|
||||||
offsetX = targetBounds.left - tooltipBounds.width - offset;
|
const triggerEl = target as HTMLElement;
|
||||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
const triggerBounds = triggerEl.getBoundingClientRect();
|
||||||
if (offsetX <= 0) {
|
|
||||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
let x = clientX;
|
||||||
}
|
let y = clientY;
|
||||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
|
||||||
|
if (tPosition === TooltipPosition.left) {
|
||||||
|
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||||
|
x = triggerBounds.left - tooltipBounds.width - offset;
|
||||||
|
y = triggerBounds.y + triggerBounds.height / 2 - tooltipBounds.height / 2;
|
||||||
|
if (x <= 0) {
|
||||||
|
x = triggerBounds.left + triggerBounds.width + offset;
|
||||||
|
}
|
||||||
|
setPos(calcTooltipPosition({ x, y }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tPosition === TooltipPosition.right) {
|
||||||
|
x = triggerBounds.left + triggerBounds.width + offset;
|
||||||
|
y = triggerBounds.y + triggerBounds.height / 2 - tooltipEl.offsetHeight / 2;
|
||||||
|
setPos(calcTooltipPosition({ x, y }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tPosition === TooltipPosition.top) {
|
||||||
|
x = triggerBounds.x + triggerBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||||
|
y = triggerBounds.top - tooltipEl.offsetHeight - offset;
|
||||||
|
setPos(calcTooltipPosition({ x, y }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tPosition === TooltipPosition.bottom) {
|
||||||
|
x = triggerBounds.x + triggerBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||||
|
y = triggerBounds.bottom + offset;
|
||||||
|
setPos(calcTooltipPosition({ x, y }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPos(calcTooltipPosition({ x, y }));
|
||||||
|
}, [calcTooltipPosition, reactEvt, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tPosition === TooltipPosition.right) {
|
if (hideTimeoutRef.current) {
|
||||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
clearTimeout(hideTimeoutRef.current);
|
||||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipEl.offsetHeight / 2;
|
hideTimeoutRef.current = null;
|
||||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
setVisible(true);
|
||||||
|
|
||||||
if (tPosition === TooltipPosition.top) {
|
if (triggerEl && tooltipRef.current) {
|
||||||
offsetY = targetBounds.top - tooltipEl.offsetHeight - offset;
|
const rect = triggerEl.getBoundingClientRect();
|
||||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
const tooltipEl = tooltipRef.current;
|
||||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tPosition === TooltipPosition.bottom) {
|
let x = evt.clientX;
|
||||||
offsetY = targetBounds.bottom + offset;
|
let y = evt.clientY;
|
||||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
|
||||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
switch (tPosition) {
|
||||||
}, [calcTooltipPosition, ev, tPosition, offset]);
|
case TooltipPosition.left: {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!targetSelector) return;
|
|
||||||
|
|
||||||
function handleMouseMove(nativeEvt: globalThis.MouseEvent) {
|
|
||||||
const targetEl = nativeEvt.target as HTMLElement | null;
|
|
||||||
if (!targetEl) {
|
|
||||||
setVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const triggerEl = targetEl.closest(targetSelector!);
|
|
||||||
const isInsideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
|
||||||
|
|
||||||
if (!triggerEl && !isInsideTooltip) {
|
|
||||||
setVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setVisible(true);
|
|
||||||
|
|
||||||
if (triggerEl && tooltipRef.current) {
|
|
||||||
const rect = triggerEl.getBoundingClientRect();
|
|
||||||
const tooltipEl = tooltipRef.current;
|
|
||||||
let x = nativeEvt.clientX;
|
|
||||||
let y = nativeEvt.clientY;
|
|
||||||
|
|
||||||
if (tPosition === TooltipPosition.left) {
|
|
||||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||||
if (x <= 0) {
|
if (x <= 0) {
|
||||||
x = rect.left + rect.width + offset;
|
x = rect.left + rect.width + offset;
|
||||||
}
|
}
|
||||||
} else if (tPosition === TooltipPosition.right) {
|
break;
|
||||||
|
}
|
||||||
|
case TooltipPosition.right: {
|
||||||
x = rect.left + rect.width + offset;
|
x = rect.left + rect.width + offset;
|
||||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||||
} else if (tPosition === TooltipPosition.top) {
|
break;
|
||||||
|
}
|
||||||
|
case TooltipPosition.top: {
|
||||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||||
} else if (tPosition === TooltipPosition.bottom) {
|
break;
|
||||||
|
}
|
||||||
|
case TooltipPosition.bottom: {
|
||||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||||
y = rect.bottom + offset;
|
y = rect.bottom + offset;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
setPos(calcTooltipPosition({ x, y }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPos(calcTooltipPosition({ x, y }));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const debounced = debounce(handleMouseMove, 10);
|
const debounced = debounce(handleMouseMove, 15);
|
||||||
|
const listener = (evt: Event) => {
|
||||||
|
debounced(evt as MouseEvent);
|
||||||
|
};
|
||||||
|
|
||||||
const listener: EventListener = evt => {
|
document.addEventListener('mousemove', listener);
|
||||||
debounced(evt as globalThis.MouseEvent);
|
return () => {
|
||||||
};
|
document.removeEventListener('mousemove', listener);
|
||||||
|
debounced.cancel();
|
||||||
|
};
|
||||||
|
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||||
|
|
||||||
document.addEventListener('mousemove', listener);
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', listener);
|
if (hideTimeoutRef.current) {
|
||||||
};
|
clearTimeout(hideTimeoutRef.current);
|
||||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition]);
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return createPortal(
|
if (!visible) return null;
|
||||||
visible && (
|
|
||||||
<div
|
return createPortal(
|
||||||
ref={tooltipRef}
|
<div
|
||||||
className={clsx(
|
ref={tooltipRef}
|
||||||
classes.tooltip,
|
className={clsx(
|
||||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
classes.tooltip,
|
||||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||||
pos === null ? 'invisible' : '',
|
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||||
className,
|
pos === null ? 'invisible' : '',
|
||||||
)}
|
className,
|
||||||
style={{
|
)}
|
||||||
top: pos?.top ?? 0,
|
style={{
|
||||||
left: pos?.left ?? 0,
|
top: pos?.top ?? 0,
|
||||||
zIndex: 10000,
|
left: pos?.left ?? 0,
|
||||||
}}
|
zIndex: 10000,
|
||||||
onMouseEnter={() => interactive && setIsMouseInsideTooltip(true)}
|
}}
|
||||||
onMouseLeave={() => interactive && setIsMouseInsideTooltip(false)}
|
onMouseEnter={() => {
|
||||||
>
|
if (interactive && hideTimeoutRef.current) {
|
||||||
{typeof content === 'function' ? content() : content}
|
clearTimeout(hideTimeoutRef.current);
|
||||||
</div>
|
hideTimeoutRef.current = null;
|
||||||
),
|
}
|
||||||
document.body,
|
setIsMouseInsideTooltip(true);
|
||||||
);
|
}}
|
||||||
},
|
onMouseLeave={() => {
|
||||||
);
|
setIsMouseInsideTooltip(false);
|
||||||
|
if (interactive) {
|
||||||
|
scheduleHide();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{typeof content === 'function' ? content() : content}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
WdTooltip.displayName = 'WdTooltip';
|
WdTooltip.displayName = 'WdTooltip';
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
/* WdTooltipWrapper.module.scss */
|
||||||
|
|
||||||
.WdTooltipWrapperRoot {
|
.WdTooltipWrapperRoot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wdTooltipSizeXs {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wdTooltipSizeSm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wdTooltipSizeMd {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wdTooltipSizeLg {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
min-width: 350px;
|
||||||
|
}
|
||||||
@@ -82,5 +82,6 @@
|
|||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC",
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1346,9 +1346,9 @@ camelcase-css@^2.0.1:
|
|||||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
||||||
version "1.0.30001600"
|
version "1.0.30001696"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz"
|
||||||
integrity sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==
|
integrity sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==
|
||||||
|
|
||||||
chalk@^2.4.2:
|
chalk@^2.4.2:
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ defmodule WandererApp.Character.TransactionsTracker do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(:shutdown, %Impl{} = state) do
|
def handle_info(:shutdown, %Impl{} = state) do
|
||||||
Logger.debug("Shutting down character transaction tracker: #{inspect(state.character_id)}")
|
Logger.debug(fn -> "Shutting down character transaction tracker: #{inspect(state.character_id)}" end)
|
||||||
{:stop, :normal, state}
|
{:stop, :normal, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
|||||||
|> Enum.map(&elem(&1, 0))
|
|> Enum.map(&elem(&1, 0))
|
||||||
|
|
||||||
if changed_systems == [] do
|
if changed_systems == [] do
|
||||||
Logger.debug("[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}")
|
Logger.debug(fn -> "[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}" end)
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
# Build new details for each changed system
|
# Build new details for each changed system
|
||||||
@@ -200,7 +200,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
|||||||
if WandererApp.Cache.lookup!("map_#{map_id}:started", false) do
|
if WandererApp.Cache.lookup!("map_#{map_id}:started", false) do
|
||||||
fun.()
|
fun.()
|
||||||
else
|
else
|
||||||
Logger.debug("[ZkbDataFetcher] Map #{map_id} not started => skipping #{label}")
|
Logger.debug(fn -> "[ZkbDataFetcher] Map #{map_id} not started => skipping #{label}" end)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
|||||||
end
|
end
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
Logger.debug("Skip adding system: #{inspect(error, pretty: true)}")
|
Logger.debug(fn -> "Skip adding system: #{inspect(error, pretty: true)}" end)
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,17 +61,15 @@ defmodule WandererApp.Structure do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp parse_end_time(str) when is_binary(str) do
|
defp parse_end_time(str) when is_binary(str) do
|
||||||
# Log everything we can about the incoming string
|
Logger.debug(fn -> "[parse_end_time] raw input => #{inspect(str)} (length=#{String.length(str)})" end)
|
||||||
Logger.debug("[parse_end_time] raw input => #{inspect(str)} (length=#{String.length(str)})")
|
|
||||||
|
|
||||||
if String.trim(str) == "" do
|
if String.trim(str) == "" do
|
||||||
Logger.debug("[parse_end_time] It's empty (or whitespace only). Returning nil.")
|
|
||||||
nil
|
nil
|
||||||
else
|
else
|
||||||
# Attempt to parse
|
# Attempt to parse
|
||||||
case DateTime.from_iso8601(str) do
|
case DateTime.from_iso8601(str) do
|
||||||
{:ok, dt, _offset} ->
|
{:ok, dt, _offset} ->
|
||||||
Logger.debug("[parse_end_time] Successfully parsed => #{inspect(dt)}")
|
|
||||||
dt
|
dt
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
|
|||||||
@@ -71,12 +71,10 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
|||||||
system_tuples = gather_visible_systems(active_maps_with_subscription)
|
system_tuples = gather_visible_systems(active_maps_with_subscription)
|
||||||
unique_systems = Enum.uniq(system_tuples)
|
unique_systems = Enum.uniq(system_tuples)
|
||||||
|
|
||||||
Logger.debug(fn ->
|
Logger.debug(fn -> "
|
||||||
"""
|
[KillsPreloader] Found #{length(unique_systems)} unique systems \
|
||||||
[KillsPreloader] Found #{length(unique_systems)} unique systems \
|
across #{length(last_active_maps)} map(s)
|
||||||
across #{length(active_maps_with_subscription)} map(s)
|
" end)
|
||||||
"""
|
|
||||||
end)
|
|
||||||
|
|
||||||
# ---- QUICK PASS ----
|
# ---- QUICK PASS ----
|
||||||
state_quick = %{state | phase: :quick_pass}
|
state_quick = %{state | phase: :quick_pass}
|
||||||
@@ -185,9 +183,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_kills_for_system(system_id, :quick, hours, limit, state) do
|
defp fetch_kills_for_system(system_id, :quick, hours, limit, state) do
|
||||||
Logger.debug(fn ->
|
Logger.debug(fn -> "[KillsPreloader] Quick fetch => system=#{system_id}, hours=#{hours}, limit=#{limit}" end)
|
||||||
"[KillsPreloader] Quick fetch => system=#{system_id}, hours=#{hours}, limit=#{limit}"
|
|
||||||
end)
|
|
||||||
|
|
||||||
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
@@ -206,9 +202,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_kills_for_system(system_id, :expanded, hours, limit, state) do
|
defp fetch_kills_for_system(system_id, :expanded, hours, limit, state) do
|
||||||
Logger.debug(fn ->
|
Logger.debug(fn -> "[KillsPreloader] Expanded fetch => system=#{system_id}, hours=#{hours}, limit=#{limit} (forcing refresh)" end)
|
||||||
"[KillsPreloader] Expanded fetch => system=#{system_id}, hours=#{hours}, limit=#{limit} (forcing refresh)"
|
|
||||||
end)
|
|
||||||
|
|
||||||
with {:ok, kills_1h, updated_state} <-
|
with {:ok, kills_1h, updated_state} <-
|
||||||
KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
||||||
@@ -232,10 +226,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
|||||||
defp maybe_fetch_more_if_needed(system_id, kills_1h, limit, state) do
|
defp maybe_fetch_more_if_needed(system_id, kills_1h, limit, state) do
|
||||||
if length(kills_1h) < limit do
|
if length(kills_1h) < limit do
|
||||||
needed = limit - length(kills_1h)
|
needed = limit - length(kills_1h)
|
||||||
|
Logger.debug(fn -> "[KillsPreloader] Expanding to #{@expanded_hours}h => system=#{system_id}, need=#{needed} more kills" end)
|
||||||
Logger.debug(fn ->
|
|
||||||
"[KillsPreloader] Expanding to #{@expanded_hours}h => system=#{system_id}, need=#{needed} more kills"
|
|
||||||
end)
|
|
||||||
|
|
||||||
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, @expanded_hours, state,
|
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, @expanded_hours, state,
|
||||||
limit: needed,
|
limit: needed,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Zkb.KillsProvider.KillsCache do
|
|||||||
Store the killmail data, keyed by killmail_id, with a 24h TTL.
|
Store the killmail data, keyed by killmail_id, with a 24h TTL.
|
||||||
"""
|
"""
|
||||||
def put_killmail(killmail_id, kill_data) do
|
def put_killmail(killmail_id, kill_data) do
|
||||||
Logger.debug("[KillsCache] Storing killmail => killmail_id=#{killmail_id}")
|
Logger.debug(fn -> "[KillsCache] Storing killmail => killmail_id=#{killmail_id}" end)
|
||||||
Cache.put(killmail_key(killmail_id), kill_data, ttl: @killmail_ttl)
|
Cache.put(killmail_key(killmail_id), kill_data, ttl: @killmail_ttl)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,8 +30,7 @@ defmodule WandererApp.Zkb.KillsProvider.KillsCache do
|
|||||||
"""
|
"""
|
||||||
def fetch_cached_kills(system_id) do
|
def fetch_cached_kills(system_id) do
|
||||||
killmail_ids = get_system_killmail_ids(system_id)
|
killmail_ids = get_system_killmail_ids(system_id)
|
||||||
# Debug-level log for performance checks
|
Logger.debug(fn -> "[KillsCache] fetch_cached_kills => system_id=#{system_id}, count=#{length(killmail_ids)}" end)
|
||||||
Logger.debug("[KillsCache] fetch_cached_kills => system_id=#{system_id}, count=#{length(killmail_ids)}")
|
|
||||||
|
|
||||||
killmail_ids
|
killmail_ids
|
||||||
|> Enum.map(&get_killmail/1)
|
|> Enum.map(&get_killmail/1)
|
||||||
@@ -130,7 +129,7 @@ defmodule WandererApp.Zkb.KillsProvider.KillsCache do
|
|||||||
final_expiry_ms = max(@base_full_fetch_expiry_ms + offset, 60_000)
|
final_expiry_ms = max(@base_full_fetch_expiry_ms + offset, 60_000)
|
||||||
expires_at_ms = now_ms + final_expiry_ms
|
expires_at_ms = now_ms + final_expiry_ms
|
||||||
|
|
||||||
Logger.debug("[KillsCache] Marking system=#{system_id} recently_fetched? until #{expires_at_ms} (ms)")
|
Logger.debug(fn -> "[KillsCache] Marking system=#{system_id} recently_fetched? until #{expires_at_ms} (ms)" end)
|
||||||
Cache.put(fetched_timestamp_key(system_id), expires_at_ms)
|
Cache.put(fetched_timestamp_key(system_id), expires_at_ms)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
|||||||
{Map.put(acc_map, sid, kills), new_st}
|
{Map.put(acc_map, sid, kills), new_st}
|
||||||
|
|
||||||
{:error, reason, new_st} ->
|
{:error, reason, new_st} ->
|
||||||
Logger.debug("[Fetcher] system=#{sid} => error=#{inspect(reason)}")
|
Logger.debug(fn -> "[Fetcher] system=#{sid} => error=#{inspect(reason)}" end)
|
||||||
{Map.put(acc_map, sid, {:error, reason}), new_st}
|
{Map.put(acc_map, sid, {:error, reason}), new_st}
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Logger.debug("[Fetcher] fetch_kills_for_systems => done, final_map_size=#{map_size(final_map)} calls=#{final_state.calls_count}")
|
Logger.debug(fn -> "[Fetcher] fetch_kills_for_systems => done, final_map_size=#{map_size(final_map)} calls=#{final_state.calls_count}" end)
|
||||||
{:ok, final_map}
|
{:ok, final_map}
|
||||||
rescue
|
rescue
|
||||||
e ->
|
e ->
|
||||||
@@ -57,10 +57,10 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
|||||||
if not force? and KillsCache.recently_fetched?(system_id) do
|
if not force? and KillsCache.recently_fetched?(system_id) do
|
||||||
cached_kills = KillsCache.fetch_cached_kills(system_id)
|
cached_kills = KillsCache.fetch_cached_kills(system_id)
|
||||||
final = maybe_take(cached_kills, limit)
|
final = maybe_take(cached_kills, limit)
|
||||||
Logger.debug("#{log_prefix}, recently_fetched?=true => returning #{length(final)} cached kills")
|
Logger.debug(fn -> "#{log_prefix}, recently_fetched?=true => returning #{length(final)} cached kills" end)
|
||||||
{:ok, final, state}
|
{:ok, final, state}
|
||||||
else
|
else
|
||||||
Logger.debug("#{log_prefix}, hours=#{since_hours}, limit=#{inspect(limit)}, force=#{force?}")
|
Logger.debug(fn -> "#{log_prefix}, hours=#{since_hours}, limit=#{inspect(limit)}, force=#{force?}" end)
|
||||||
|
|
||||||
cutoff_dt = hours_ago(since_hours)
|
cutoff_dt = hours_ago(since_hours)
|
||||||
|
|
||||||
@@ -75,9 +75,9 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
|||||||
KillsCache.put_full_fetched_timestamp(system_id)
|
KillsCache.put_full_fetched_timestamp(system_id)
|
||||||
final_kills = KillsCache.fetch_cached_kills(system_id) |> maybe_take(limit)
|
final_kills = KillsCache.fetch_cached_kills(system_id) |> maybe_take(limit)
|
||||||
|
|
||||||
Logger.debug(
|
Logger.debug(fn ->
|
||||||
"#{log_prefix}, total_fetched=#{total_fetched}, final_cached=#{length(final_kills)}, calls_count=#{new_st.calls_count}"
|
"#{log_prefix}, total_fetched=#{total_fetched}, final_cached=#{length(final_kills)}, calls_count=#{new_st.calls_count}"
|
||||||
)
|
end)
|
||||||
|
|
||||||
{:ok, final_kills, new_st}
|
{:ok, final_kills, new_st}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
|||||||
|
|
||||||
with {:ok, st1} <- increment_calls_count(state),
|
with {:ok, st1} <- increment_calls_count(state),
|
||||||
{:ok, st2, partials} <- ZkbApi.fetch_and_parse_page(system_id, page, st1) do
|
{:ok, st2, partials} <- ZkbApi.fetch_and_parse_page(system_id, page, st1) do
|
||||||
Logger.debug("[Fetcher] system=#{system_id}, page=#{page}, partials_count=#{length(partials)}")
|
Logger.debug(fn -> "[Fetcher] system=#{system_id}, page=#{page}, partials_count=#{length(partials)}" end)
|
||||||
|
|
||||||
{_count_stored, older_found?, total_now} =
|
{_count_stored, older_found?, total_now} =
|
||||||
Enum.reduce_while(partials, {0, false, total_so_far}, fn partial, {acc_count, had_older, acc_total} ->
|
Enum.reduce_while(partials, {0, false, total_so_far}, fn partial, {acc_count, had_older, acc_total} ->
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
|||||||
|
|
||||||
# Called by `KillsProvider.handle_in`
|
# Called by `KillsProvider.handle_in`
|
||||||
def handle_in({:text, frame}, state) do
|
def handle_in({:text, frame}, state) do
|
||||||
Logger.debug("[KillsProvider.Websocket] Received frame => #{frame}")
|
Logger.debug(fn -> "[KillsProvider.Websocket] Received frame => #{frame}" end)
|
||||||
partial = Jason.decode!(frame)
|
partial = Jason.decode!(frame)
|
||||||
parse_and_store_zkb_partial(partial)
|
parse_and_store_zkb_partial(partial)
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
@@ -61,14 +61,14 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp handle_subscribe(channel, state) do
|
defp handle_subscribe(channel, state) do
|
||||||
Logger.debug("[KillsProvider.Websocket] Subscribing to #{channel}")
|
Logger.debug(fn -> "[KillsProvider.Websocket] Subscribing to #{channel}" end)
|
||||||
payload = Jason.encode!(%{"action" => "sub", "channel" => channel})
|
payload = Jason.encode!(%{"action" => "sub", "channel" => channel})
|
||||||
{:reply, {:text, payload}, state}
|
{:reply, {:text, payload}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
|
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
|
||||||
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
|
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
|
||||||
Logger.debug("[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}")
|
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
|
||||||
case Esi.get_killmail(kill_id, kill_hash) do
|
case Esi.get_killmail(kill_id, kill_hash) do
|
||||||
{:ok, full_esi_data} ->
|
{:ok, full_esi_data} ->
|
||||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||||
|
|||||||
Reference in New Issue
Block a user