mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 02:35:42 +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 { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
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');
|
||||
|
||||
// You can create nodeTypes dynamically based on the node component
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom: nodeComponent,
|
||||
@@ -256,7 +255,7 @@ const MapComp = ({
|
||||
|
||||
onEdgesChange(nextChanges);
|
||||
},
|
||||
[getEdge, getNode, onEdgesChange],
|
||||
[canRemoveConnection, getEdge, getNode, onEdgesChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -283,6 +282,7 @@ const MapComp = ({
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={connectionMode}
|
||||
snapToGrid
|
||||
snapGrid={[snapSize, snapSize]}
|
||||
nodeDragThreshold={10}
|
||||
onNodeDragStop={handleDragStop}
|
||||
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-pink: #d291bc;
|
||||
$pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$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 {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
height: 34px;
|
||||
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 2px 6px;
|
||||
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);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
@@ -92,14 +88,6 @@ $custom-name: #93C5FD;
|
||||
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 {
|
||||
border: 1px solid var(--eve-solar-system-status-color-home-dark30);
|
||||
background-image: linear-gradient(
|
||||
@@ -178,8 +166,6 @@ $custom-name: #93C5FD;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
//background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -266,26 +252,18 @@ $custom-name: #93C5FD;
|
||||
|
||||
.TagTitle {
|
||||
font-size: 11px;
|
||||
font-weight: medium;
|
||||
font-weight: 500;
|
||||
text-shadow: 0 0 2px rgba(231, 146, 52, 0.73);
|
||||
|
||||
color: var(--rf-tag-color, #38BDF8);
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
.classSystemName {
|
||||
font-family: inherit !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.classSystemName {
|
||||
//font-weight: bold;
|
||||
}
|
||||
|
||||
.solarSystemName {
|
||||
}
|
||||
}
|
||||
|
||||
.BottomRow {
|
||||
@@ -294,22 +272,23 @@ $custom-name: #93C5FD;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
|
||||
.localCounter {
|
||||
display: flex;
|
||||
//align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
.hasLocalCounter {
|
||||
margin-right: 1.25rem;
|
||||
&.countAbove9 {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: 500;
|
||||
//margin-top: 1px;
|
||||
}
|
||||
.lockIcon {
|
||||
font-size: 0.45rem;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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 { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
import { useLocalCounter, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,7 +25,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
<div className={classes.Bookmarks}>
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -32,13 +35,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
||||
{nodeVars.killsCount && nodeVars.killsCount > 0 && nodeVars.solarSystemId && (
|
||||
<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)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
@@ -53,10 +62,8 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
@@ -88,7 +95,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -114,24 +121,15 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{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
|
||||
className={clsx('flex items-center gap-1', {
|
||||
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||
})}
|
||||
>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +143,7 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -153,14 +151,14 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
@@ -202,6 +200,11 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
id="d"
|
||||
/>
|
||||
</div>
|
||||
<LocalCounter
|
||||
hasUserCharacters={nodeVars.hasUserCharacters}
|
||||
localCounterCharacters={localCounterCharacters}
|
||||
classes={classes}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,91 +1,6 @@
|
||||
@import './SolarSystemNodeDefault.module.scss';
|
||||
|
||||
/* ---------------------------
|
||||
Only override what's different
|
||||
--------------------------- */
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ---------------------------------------------
|
||||
Only override what's different from the base
|
||||
Currently none required
|
||||
---------------------------------------------- */
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { memo } from 'react';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
import { useLocalCounter, useSolarSystemNode } from '../../hooks/useSolarSystemLogic';
|
||||
import {
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
EFFECT_BACKGROUND_STYLES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { LocalCounter } from './SolarSystemLocalCounter';
|
||||
import { KillsCounter } from './SolarSystemKillsCounter';
|
||||
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
const { localCounterCharacters } = useLocalCounter(nodeVars);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -32,13 +35,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[nodeVars.killsActivityType!])}>
|
||||
{nodeVars.killsCount && nodeVars.killsCount > 0 && nodeVars.solarSystemId && (
|
||||
<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)}>
|
||||
<span className={clsx(PrimeIcons.BOLT, classes.icon)} />
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</KillsCounter>
|
||||
)}
|
||||
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
@@ -53,10 +62,8 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
nodeVars.status !== undefined ? classes[STATUS_CLASSES[nodeVars.status]] : '',
|
||||
{ [classes.selected]: nodeVars.selected },
|
||||
)}
|
||||
>
|
||||
{nodeVars.visible && (
|
||||
@@ -88,7 +95,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
<WormholeClassComp key={String(whClass)} id={String(whClass)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -124,24 +131,15 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{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
|
||||
className={clsx('flex items-center gap-1', {
|
||||
[classes.hasLocalCounter]: nodeVars.charactersInSystem.length > 0,
|
||||
[classes.countAbove9]: nodeVars.charactersInSystem.length > 9,
|
||||
})}
|
||||
>
|
||||
{nodeVars.locked && <i className={clsx(PrimeIcons.LOCK, classes.lockIcon)} />}
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId) && (
|
||||
<i className={clsx(PrimeIcons.MAP_MARKER, classes.mapMarker)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +153,7 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -163,14 +161,14 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<div onMouseDownCapture={e => nodeVars.dbClick(e)} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
@@ -212,6 +210,11 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
id="d"
|
||||
/>
|
||||
</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 [size, setSize] = useState<number>(1);
|
||||
const [color, setColor] = useState('#81818b');
|
||||
const [snapSize, setSnapSize] = useState<number>(25);
|
||||
|
||||
useEffect(() => {
|
||||
// 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 cssVarSize = style.getPropertyValue('--rf-bg-size');
|
||||
const cssVarSnapSize = style.getPropertyValue('--rf-snap-size');
|
||||
const cssColor = style.getPropertyValue('--rf-bg-pattern-color');
|
||||
|
||||
const gapNum = parseInt(cssVarGap, 10) || 16;
|
||||
const sizeNum = parseInt(cssVarSize, 10) || 1;
|
||||
const snapSize = parseInt(cssVarSnapSize, 10) || 25; //react-flow default
|
||||
|
||||
setVariant(finalVariant);
|
||||
setGap(gapNum);
|
||||
setSize(sizeNum);
|
||||
setColor(cssColor);
|
||||
setSnapSize(snapSize);
|
||||
}, [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
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Map handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -10,10 +10,17 @@ import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormhol
|
||||
import { getSystemClassStyles, prepareUnsplashedChunks } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
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';
|
||||
|
||||
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 <= 30) return 'activityWarn';
|
||||
return 'activityDanger';
|
||||
@@ -26,12 +33,25 @@ const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Gallente]: 'Gallente',
|
||||
};
|
||||
|
||||
function sortedLabels(labels: string[]) {
|
||||
function sortedLabels(labels: string[]): LabelInfo[] {
|
||||
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 {
|
||||
system_static_info,
|
||||
@@ -71,7 +91,6 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const {
|
||||
data: {
|
||||
characters,
|
||||
presentCharacters,
|
||||
wormholesData,
|
||||
hubs,
|
||||
kills,
|
||||
@@ -87,15 +106,14 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const systemSignatures = useMemo(
|
||||
const systemSigs = useMemo(
|
||||
() => mapSystemSignatures[solar_system_id] || system_signatures,
|
||||
[system_signatures, solar_system_id, mapSystemSignatures],
|
||||
);
|
||||
|
||||
const charactersInSystem = useMemo(() => {
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id).filter(c => c.online);
|
||||
// eslint-disable-next-line
|
||||
}, [characters, presentCharacters, solar_system_id]);
|
||||
return characters.filter(c => c.location?.solar_system_id === solar_system_id && c.online);
|
||||
}, [characters, solar_system_id]);
|
||||
|
||||
const isWormhole = isWormholeSpace(system_class);
|
||||
|
||||
@@ -136,52 +154,65 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
|
||||
const temporaryName = useMemo(() => {
|
||||
const computedTemporaryName = useMemo(() => {
|
||||
if (!isTempSystemNameEnabled) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
}
|
||||
|
||||
return temporary_name;
|
||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
||||
|
||||
const systemName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && temporaryName) {
|
||||
return temporaryName;
|
||||
if (isTempSystemNameEnabled && computedTemporaryName) {
|
||||
return computedTemporaryName;
|
||||
}
|
||||
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(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
return [[], []];
|
||||
}
|
||||
return prepareUnsplashedChunks(
|
||||
systemSignatures
|
||||
systemSigs
|
||||
.filter(s => s.group === 'Wormhole' && !s.linked_system)
|
||||
.map(s => ({
|
||||
eve_id: s.eve_id,
|
||||
type: s.type,
|
||||
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,
|
||||
selected,
|
||||
|
||||
visible,
|
||||
isWormhole,
|
||||
classTitleColor,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
hasUserCharacters,
|
||||
userCharacters,
|
||||
showHandlers,
|
||||
regionClass,
|
||||
systemName,
|
||||
@@ -195,10 +226,10 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
sortedStatics,
|
||||
effectName: effect_name,
|
||||
regionName: region_name,
|
||||
solarSystemId: solar_system_id,
|
||||
solarSystemId: solar_system_id.toString(),
|
||||
solarSystemName: solar_system_name,
|
||||
locked,
|
||||
hubs,
|
||||
hubs: hubsAsStrings,
|
||||
name: name,
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
@@ -207,7 +238,7 @@ export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
unsplashedRight,
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: temporary_name,
|
||||
temporaryName: computedTemporaryName,
|
||||
};
|
||||
|
||||
return nodeVars;
|
||||
@@ -230,24 +261,22 @@ export interface SolarSystemNodeVars {
|
||||
isShattered: boolean;
|
||||
tag?: string | null;
|
||||
status?: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
labelsInfo: Array<any>;
|
||||
dbClick: (event?: void) => void;
|
||||
labelsInfo: LabelInfo[];
|
||||
dbClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
sortedStatics: Array<string | number>;
|
||||
effectName: string | null;
|
||||
regionName: string | null;
|
||||
solarSystemId: number;
|
||||
solarSystemId: string;
|
||||
solarSystemName: string | null;
|
||||
locked: boolean;
|
||||
hubs: string[] | number[];
|
||||
hubs: string[];
|
||||
name: string | null;
|
||||
isConnecting: boolean;
|
||||
hoverNodeId: string | null;
|
||||
charactersInSystem: Array<CharacterTypeRaw>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
unsplashedLeft: Array<any>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
unsplashedRight: Array<any>;
|
||||
userCharacters: string[];
|
||||
unsplashedLeft: Array<SystemSignature>;
|
||||
unsplashedRight: Array<SystemSignature>;
|
||||
isThickConnections: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
@@ -11,7 +11,8 @@
|
||||
--rf-tag-color: #38BDF8;
|
||||
--rf-region-name: #D6D3D1;
|
||||
--rf-custom-name: #93C5FD;
|
||||
|
||||
--rf-node-font-family: 'Shentox', 'Rogan', sans-serif !important;
|
||||
--rf-node-font-weight: 500;
|
||||
|
||||
--rf-bg-variant: "dots";
|
||||
--rf-bg-gap: 15;
|
||||
@@ -28,4 +29,8 @@
|
||||
--tooltip-bg: #202020;
|
||||
|
||||
--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');
|
||||
|
||||
.pathfinder-theme {
|
||||
/* -- Override values from the default theme -- */
|
||||
--rf-bg-color: #000000;
|
||||
--rf-soft-bg-color: #282828;
|
||||
|
||||
--rf-node-bg-color: #202020;
|
||||
--rf-node-soft-bg-color: #313335;
|
||||
--rf-node-font-weight: bold;
|
||||
--rf-text-color: #adadad;
|
||||
--rf-region-name: var(--rf-text-color);
|
||||
--rf-custom-name: var(--rf-text-color);
|
||||
|
||||
--tooltip-bg: #202020;
|
||||
|
||||
--rf-bg-variant: "lines";
|
||||
--rf-bg-gap: 32;
|
||||
--rf-bg-size: 1;
|
||||
--rf-bg-gap: 34;
|
||||
--rf-snap-size: 17;
|
||||
--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-magnetar: #e06fdf;
|
||||
--eve-effect-wolfRayet: #e28a0d;
|
||||
@@ -38,14 +41,4 @@
|
||||
--eve-wh-type-color-c6: #d9534f;
|
||||
--eve-wh-type-color-c13: #7986cb;
|
||||
--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 { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import classes from './LocalCharacters.module.scss';
|
||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||
import { CharacterCard, LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { LayoutEventBlocker, WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
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 { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
type CharItemProps = {
|
||||
compact: boolean;
|
||||
} & 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,
|
||||
};
|
||||
import { LocalCharactersList } from './components/LocalCharactersList';
|
||||
import { useLocalCharactersItemTemplate } from './hooks/useLocalCharacters';
|
||||
import { useLocalCharacterWidgetSettings } from './hooks/useLocalWidgetSettings';
|
||||
|
||||
export const LocalCharacters = () => {
|
||||
const {
|
||||
data: { characters, userCharacters, selectedSystems, presentCharacters },
|
||||
data: { characters, userCharacters, selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const [settings, setSettings] = useLocalStorageState<WindowLocalSettingsType>('window:local:settings', {
|
||||
defaultValue: STORED_DEFAULT_VALUES,
|
||||
});
|
||||
const [settings, setSettings] = useLocalCharacterWidgetSettings();
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
|
||||
const restrictOfflineShowing = useMapGetOption('restrict_offline_showing');
|
||||
const isAdminOrManager = useMapCheckPermissions([UserPermission.MANAGE_MAP]);
|
||||
|
||||
@@ -74,21 +28,31 @@ export const LocalCharacters = () => {
|
||||
[isAdminOrManager, restrictOfflineShowing],
|
||||
);
|
||||
|
||||
const itemTemplate = useItemTemplate();
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const sorted = characters
|
||||
const filtered = characters
|
||||
.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);
|
||||
|
||||
if (!showOffline || !settings.showOffline) {
|
||||
return sorted.filter(c => c.online);
|
||||
return filtered.filter(c => c.online);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
// eslint-disable-next-line
|
||||
}, [showOffline, characters, settings.showOffline, settings.compact, systemId, userCharacters, presentCharacters]);
|
||||
return filtered;
|
||||
}, [
|
||||
characters,
|
||||
systemId,
|
||||
userCharacters,
|
||||
settings.compact,
|
||||
settings.showOffline,
|
||||
settings.showShipName,
|
||||
showOffline,
|
||||
]);
|
||||
|
||||
const isNobodyHere = sorted.length === 0;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
@@ -97,6 +61,8 @@ export const LocalCharacters = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 145);
|
||||
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
@@ -111,7 +77,20 @@ export const LocalCharacters = () => {
|
||||
label={compact ? '' : 'Show offline'}
|
||||
value={settings.showOffline}
|
||||
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>
|
||||
)}
|
||||
@@ -121,8 +100,8 @@ export const LocalCharacters = () => {
|
||||
['hero-bars-2']: settings.compact,
|
||||
['hero-bars-3']: !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings(() => ({ ...settings, compact: !settings.compact }))}
|
||||
></span>
|
||||
onClick={() => setSettings(prev => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
}
|
||||
@@ -140,15 +119,11 @@ export const LocalCharacters = () => {
|
||||
)}
|
||||
|
||||
{showList && (
|
||||
<VirtualScroller
|
||||
<LocalCharactersList
|
||||
items={sorted}
|
||||
itemSize={settings.compact ? 26 : 41}
|
||||
itemTemplate={itemTemplate}
|
||||
className={clsx(
|
||||
classes.VirtualScroller,
|
||||
'w-full h-full overflow-x-hidden overflow-y-auto custom-scrollbar select-none',
|
||||
)}
|
||||
autoSize={false}
|
||||
containerClassName="w-full h-full overflow-x-hidden overflow-y-auto"
|
||||
/>
|
||||
)}
|
||||
</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 = {
|
||||
compact: false,
|
||||
compact: true,
|
||||
showAll: false,
|
||||
excludedSystems: [],
|
||||
version: 0,
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
.CharIcon {
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid #2b2b2b;
|
||||
}
|
||||
|
||||
.CharRow {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
type CharacterCardProps = {
|
||||
compact?: boolean;
|
||||
showShipName?: boolean;
|
||||
showSystem?: boolean;
|
||||
showShipName?: boolean;
|
||||
useSystemsCache?: boolean;
|
||||
} & CharacterTypeRaw &
|
||||
WithIsOwnCharacter;
|
||||
@@ -22,8 +22,15 @@ export const getShipName = (name: string) => {
|
||||
.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 = ({
|
||||
compact,
|
||||
compact = false,
|
||||
isOwn,
|
||||
showSystem,
|
||||
showShipName,
|
||||
@@ -37,59 +44,135 @@ export const CharacterCard = ({
|
||||
});
|
||||
}, [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 (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')} onClick={handleSelect}>
|
||||
<div className="flex px-2 py-1 gap-1">
|
||||
{!compact && (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs box-border')} onClick={handleSelect}>
|
||||
<div
|
||||
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
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{
|
||||
// The SCSS below ensures the image is square with a border.
|
||||
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
|
||||
className={clsx(classes.CharRow, 'w-full', {
|
||||
[classes.TwoColumns]: !char.ship,
|
||||
[classes.ThreeColumns]: char.ship,
|
||||
className={clsx('overflow-hidden whitespace-nowrap', {
|
||||
[classes.CardBorderLeftIsOwn]: isOwn,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
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>
|
||||
)}
|
||||
{compact ? compactLine : nonCompactLine1}
|
||||
</div>
|
||||
|
||||
{showShipName && !compact && char.ship?.ship_name && (
|
||||
<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} />
|
||||
)}
|
||||
{/* Non-compact second line always shows shipName if available */}
|
||||
{!compact && nonCompactLine2}
|
||||
</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>
|
||||
|
||||
{/*
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ export enum TooltipPosition {
|
||||
bottom = 'bottom',
|
||||
}
|
||||
|
||||
export interface TooltipProps {
|
||||
export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'content'> {
|
||||
position?: TooltipPosition;
|
||||
offset?: number;
|
||||
content: (() => React.ReactNode) | React.ReactNode;
|
||||
@@ -27,183 +27,253 @@ export interface OffsetPosition {
|
||||
|
||||
export interface WdTooltipHandlers {
|
||||
show: (e?: React.MouseEvent) => void;
|
||||
hide: (e?: React.MouseEvent) => void;
|
||||
hide: () => void;
|
||||
getIsMouseInside: () => boolean;
|
||||
}
|
||||
|
||||
export const WdTooltip = forwardRef(
|
||||
(props: TooltipProps & { className?: string }, ref: ForwardedRef<WdTooltipHandlers>) => {
|
||||
const {
|
||||
content,
|
||||
targetSelector,
|
||||
position: tPosition = TooltipPosition.default,
|
||||
className,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
} = props;
|
||||
const LEAVE_DELAY = 100;
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const [ev, setEv] = useState<React.MouseEvent>();
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
export const WdTooltip = forwardRef(function WdTooltip(
|
||||
{
|
||||
content,
|
||||
targetSelector,
|
||||
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 }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
|
||||
if (newLeft < 0) newLeft = 10;
|
||||
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 };
|
||||
}, []);
|
||||
const [reactEvt, setReactEvt] = useState<React.MouseEvent>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (mouseEvt?: React.MouseEvent) => {
|
||||
if (mouseEvt) setEv(mouseEvt);
|
||||
setPos(null);
|
||||
setVisible(true);
|
||||
},
|
||||
hide: () => {
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
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);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
}, [interactive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !ev) return;
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const { clientX, clientY, target } = ev;
|
||||
const targetBounds = (target as HTMLElement).getBoundingClientRect();
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (e?: React.MouseEvent) => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
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;
|
||||
let offsetY = clientY;
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !reactEvt) return;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
offsetX = targetBounds.left - tooltipBounds.width - offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
||||
if (offsetX <= 0) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
}
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
const { clientX, clientY, target } = reactEvt;
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const triggerEl = target as HTMLElement;
|
||||
const triggerBounds = triggerEl.getBoundingClientRect();
|
||||
|
||||
let x = clientX;
|
||||
let y = clientY;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
setVisible(true);
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
offsetY = targetBounds.top - tooltipEl.offsetHeight - offset;
|
||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
offsetY = targetBounds.bottom + offset;
|
||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
}, [calcTooltipPosition, ev, tPosition, offset]);
|
||||
|
||||
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) {
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left: {
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
} else if (tPosition === TooltipPosition.right) {
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.right: {
|
||||
x = rect.left + rect.width + offset;
|
||||
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;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
} else if (tPosition === TooltipPosition.bottom) {
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.bottom: {
|
||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
default:
|
||||
}
|
||||
|
||||
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 => {
|
||||
debounced(evt as globalThis.MouseEvent);
|
||||
};
|
||||
document.addEventListener('mousemove', listener);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', listener);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
|
||||
document.addEventListener('mousemove', listener);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', listener);
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return createPortal(
|
||||
visible && (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos === null ? 'invisible' : '',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => interactive && setIsMouseInsideTooltip(true)}
|
||||
onMouseLeave={() => interactive && setIsMouseInsideTooltip(false)}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>
|
||||
),
|
||||
document.body,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (!visible) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos === null ? 'invisible' : '',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (interactive && hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
setIsMouseInsideTooltip(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsMouseInsideTooltip(false);
|
||||
if (interactive) {
|
||||
scheduleHide();
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
});
|
||||
|
||||
WdTooltip.displayName = 'WdTooltip';
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/* WdTooltipWrapper.module.scss */
|
||||
|
||||
.WdTooltipWrapperRoot {
|
||||
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": [],
|
||||
"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==
|
||||
|
||||
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
||||
version "1.0.30001600"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz"
|
||||
integrity sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==
|
||||
version "1.0.30001696"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz"
|
||||
integrity sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==
|
||||
|
||||
chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
|
||||
@@ -71,7 +71,7 @@ defmodule WandererApp.Character.TransactionsTracker do
|
||||
|
||||
@impl true
|
||||
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}
|
||||
end
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|
||||
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
|
||||
else
|
||||
# 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
|
||||
fun.()
|
||||
else
|
||||
Logger.debug("[ZkbDataFetcher] Map #{map_id} not started => skipping #{label}")
|
||||
Logger.debug(fn -> "[ZkbDataFetcher] Map #{map_id} not started => skipping #{label}" end)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -393,7 +393,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
|
||||
error ->
|
||||
Logger.debug("Skip adding system: #{inspect(error, pretty: true)}")
|
||||
Logger.debug(fn -> "Skip adding system: #{inspect(error, pretty: true)}" end)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,17 +61,15 @@ defmodule WandererApp.Structure do
|
||||
end
|
||||
|
||||
defp parse_end_time(str) when is_binary(str) do
|
||||
# Log everything we can about the incoming string
|
||||
Logger.debug("[parse_end_time] raw input => #{inspect(str)} (length=#{String.length(str)})")
|
||||
Logger.debug(fn -> "[parse_end_time] raw input => #{inspect(str)} (length=#{String.length(str)})" end)
|
||||
|
||||
|
||||
if String.trim(str) == "" do
|
||||
Logger.debug("[parse_end_time] It's empty (or whitespace only). Returning nil.")
|
||||
nil
|
||||
else
|
||||
# Attempt to parse
|
||||
case DateTime.from_iso8601(str) do
|
||||
{:ok, dt, _offset} ->
|
||||
Logger.debug("[parse_end_time] Successfully parsed => #{inspect(dt)}")
|
||||
dt
|
||||
|
||||
{:error, reason} ->
|
||||
|
||||
@@ -71,12 +71,10 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
||||
system_tuples = gather_visible_systems(active_maps_with_subscription)
|
||||
unique_systems = Enum.uniq(system_tuples)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"""
|
||||
[KillsPreloader] Found #{length(unique_systems)} unique systems \
|
||||
across #{length(active_maps_with_subscription)} map(s)
|
||||
"""
|
||||
end)
|
||||
Logger.debug(fn -> "
|
||||
[KillsPreloader] Found #{length(unique_systems)} unique systems \
|
||||
across #{length(last_active_maps)} map(s)
|
||||
" end)
|
||||
|
||||
# ---- QUICK PASS ----
|
||||
state_quick = %{state | phase: :quick_pass}
|
||||
@@ -185,9 +183,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
||||
end
|
||||
|
||||
defp fetch_kills_for_system(system_id, :quick, hours, limit, state) do
|
||||
Logger.debug(fn ->
|
||||
"[KillsPreloader] Quick fetch => system=#{system_id}, hours=#{hours}, limit=#{limit}"
|
||||
end)
|
||||
Logger.debug(fn -> "[KillsPreloader] Quick fetch => system=#{system_id}, hours=#{hours}, limit=#{limit}" end)
|
||||
|
||||
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
||||
limit: limit,
|
||||
@@ -206,9 +202,7 @@ defmodule WandererApp.Zkb.KillsPreloader do
|
||||
end
|
||||
|
||||
defp fetch_kills_for_system(system_id, :expanded, hours, limit, state) do
|
||||
Logger.debug(fn ->
|
||||
"[KillsPreloader] Expanded fetch => system=#{system_id}, hours=#{hours}, limit=#{limit} (forcing refresh)"
|
||||
end)
|
||||
Logger.debug(fn -> "[KillsPreloader] Expanded fetch => system=#{system_id}, hours=#{hours}, limit=#{limit} (forcing refresh)" end)
|
||||
|
||||
with {:ok, kills_1h, updated_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
|
||||
if length(kills_1h) < limit do
|
||||
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,
|
||||
limit: needed,
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Zkb.KillsProvider.KillsCache do
|
||||
Store the killmail data, keyed by killmail_id, with a 24h TTL.
|
||||
"""
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -30,8 +30,7 @@ defmodule WandererApp.Zkb.KillsProvider.KillsCache do
|
||||
"""
|
||||
def fetch_cached_kills(system_id) do
|
||||
killmail_ids = get_system_killmail_ids(system_id)
|
||||
# Debug-level log for performance checks
|
||||
Logger.debug("[KillsCache] fetch_cached_kills => system_id=#{system_id}, count=#{length(killmail_ids)}")
|
||||
Logger.debug(fn -> "[KillsCache] fetch_cached_kills => system_id=#{system_id}, count=#{length(killmail_ids)}" end)
|
||||
|
||||
killmail_ids
|
||||
|> 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)
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
{Map.put(acc_map, sid, kills), 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}
|
||||
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}
|
||||
rescue
|
||||
e ->
|
||||
@@ -57,10 +57,10 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
if not force? and KillsCache.recently_fetched?(system_id) do
|
||||
cached_kills = KillsCache.fetch_cached_kills(system_id)
|
||||
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}
|
||||
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)
|
||||
|
||||
@@ -75,9 +75,9 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
KillsCache.put_full_fetched_timestamp(system_id)
|
||||
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}"
|
||||
)
|
||||
end)
|
||||
|
||||
{:ok, final_kills, new_st}
|
||||
|
||||
@@ -117,7 +117,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
||||
|
||||
with {:ok, st1} <- increment_calls_count(state),
|
||||
{: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} =
|
||||
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`
|
||||
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)
|
||||
parse_and_store_zkb_partial(partial)
|
||||
{:ok, state}
|
||||
@@ -61,14 +61,14 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
||||
end
|
||||
|
||||
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})
|
||||
{:reply, {:text, payload}, state}
|
||||
end
|
||||
|
||||
# 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
|
||||
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
|
||||
{:ok, full_esi_data} ->
|
||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||
|
||||
Reference in New Issue
Block a user