mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-02 15:37:03 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7073a0e8e6 | ||
|
|
bb0d91a3c7 | ||
|
|
1cb12b97ba | ||
|
|
860d20dc66 | ||
|
|
a850071965 | ||
|
|
fc41573e70 | ||
|
|
97f1808fb5 | ||
|
|
d31046eebb | ||
|
|
a70fa50eab | ||
|
|
9a082c26f5 | ||
|
|
6af2dc1ed5 | ||
|
|
5fd1509d44 | ||
|
|
2448c0531b | ||
|
|
b685ea1013 | ||
|
|
55465688c8 | ||
|
|
ac3c7e0c44 | ||
|
|
2d6ab5646c | ||
|
|
67b373ac29 | ||
|
|
678169e6fa | ||
|
|
7ee3c8db82 | ||
|
|
304f4b01ab | ||
|
|
4af12c21b2 | ||
|
|
497da1e5f7 | ||
|
|
5bd968acae | ||
|
|
f74c20142c | ||
|
|
d4c40d7542 | ||
|
|
04f3fec0c0 | ||
|
|
cd0b4b0fc9 | ||
|
|
e7b115e6e6 | ||
|
|
dff8fc6396 | ||
|
|
afdaeb3d34 | ||
|
|
ac6053361e | ||
|
|
eb3e1ba3aa | ||
|
|
8468a9b5de | ||
|
|
5eafe59dcb | ||
|
|
b38bcaa8cf | ||
|
|
8a238a447d | ||
|
|
3731219216 | ||
|
|
73d5fd5f67 | ||
|
|
e8e4aed6d5 |
@@ -7,3 +7,4 @@ export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
|
||||
export GIT_SHA="1111"
|
||||
export WANDERER_INVITES="false"
|
||||
export WANDERER_PUBLIC_API_DISABLED="false"
|
||||
export WANDERER_ZKILL_PRELOAD_DISABLED="false"
|
||||
|
||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -140,6 +140,7 @@ jobs:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm64/v8
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
|
||||
144
CHANGELOG.md
144
CHANGELOG.md
@@ -2,6 +2,150 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.45.2](https://github.com/wanderer-industries/wanderer/compare/v1.45.1...v1.45.2) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fix route list hover and on the map character list (#149)
|
||||
|
||||
* correct formatting for on the map character list
|
||||
|
||||
* fix hover for route list
|
||||
|
||||
## [v1.45.1](https://github.com/wanderer-industries/wanderer/compare/v1.45.0...v1.45.1) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* kill count subscript position on firefox, and remove kill filter for single system (#148)
|
||||
|
||||
## [v1.45.0](https://github.com/wanderer-industries/wanderer/compare/v1.44.9...v1.45.0) (2025-02-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* allow filtering of k-space kills (#147)
|
||||
|
||||
## [v1.44.9](https://github.com/wanderer-industries/wanderer/compare/v1.44.8...v1.44.9) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* improve local character header shrink behavior (#146)
|
||||
|
||||
## [v1.44.8](https://github.com/wanderer-industries/wanderer/compare/v1.44.7...v1.44.8) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: include external libraries in build
|
||||
|
||||
## [v1.44.7](https://github.com/wanderer-industries/wanderer/compare/v1.44.6...v1.44.7) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: include external libraries in build
|
||||
|
||||
## [v1.44.6](https://github.com/wanderer-industries/wanderer/compare/v1.44.5...v1.44.6) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.44.5](https://github.com/wanderer-industries/wanderer/compare/v1.44.4...v1.44.5) (2025-02-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* include category param in search cache key (#144)
|
||||
|
||||
## [v1.44.4](https://github.com/wanderer-industries/wanderer/compare/v1.44.3...v1.44.4) (2025-02-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.44.3](https://github.com/wanderer-industries/wanderer/compare/v1.44.2...v1.44.3) (2025-02-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* restored kills lightning bolt functionality (#143)
|
||||
|
||||
## [v1.44.2](https://github.com/wanderer-industries/wanderer/compare/v1.44.1...v1.44.2) (2025-02-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.44.1](https://github.com/wanderer-industries/wanderer/compare/v1.44.0...v1.44.1) (2025-02-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem with windows. (#140)
|
||||
|
||||
* Map: Fixed problem with windows.
|
||||
|
||||
* Core: Added min heigth for body
|
||||
|
||||
## [v1.44.0](https://github.com/wanderer-industries/wanderer/compare/v1.43.9...v1.44.0) (2025-02-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add news post for zkill widget
|
||||
|
||||
* add zkill widget
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* design feedback patch
|
||||
|
||||
* removed unneeded event handler
|
||||
|
||||
## [v1.43.9](https://github.com/wanderer-industries/wanderer/compare/v1.43.8...v1.43.9) (2025-01-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Add discord link to 'Like' icon on main interface
|
||||
|
||||
## [v1.43.8](https://github.com/wanderer-industries/wanderer/compare/v1.43.7...v1.43.8) (2025-01-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Update shuttered constellations (required EVE DB data update on server).
|
||||
|
||||
## [v1.43.7](https://github.com/wanderer-industries/wanderer/compare/v1.43.6...v1.43.7) (2025-01-26)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.6](https://github.com/wanderer-industries/wanderer/compare/v1.43.5...v1.43.6) (2025-01-22)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ Now you can visit [`localhost:8000`](http://localhost:8000) from your browser.
|
||||
- `root@0d0a785313b6:/app# apt update`
|
||||
- `root@0d0a785313b6:/app# curl -sL https://deb.nodesource.com/setup_18.x | bash -`
|
||||
- `root@0d0a785313b6:/app# apt-get install nodejs inotify-tools -y`
|
||||
- `root@0d0a785313b6:/app# npm install -g yarn`
|
||||
- `root@0d0a785313b6:/app# mix setup`
|
||||
|
||||
- See how to run server in #Run section
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// import '@fontsource-variable/inter'
|
||||
// import '@fontsource-variable/jetbrains-mono'
|
||||
// import './lib/tailwind/index.css';
|
||||
import './css/app.css';
|
||||
|
||||
import './lib/phoenix';
|
||||
|
||||
@@ -25,6 +25,10 @@ body {
|
||||
width: 400px; /* As IE6 ignores !important it will set width as 400px; */
|
||||
}
|
||||
|
||||
body > div:first-of-type {
|
||||
min-height: 500px !important;
|
||||
}
|
||||
|
||||
.lending-normal {
|
||||
font-family: 'Shentox', 'Rogan', sans-serif !important;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -112,3 +112,28 @@
|
||||
.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-token {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Fixed sizes of Input switch */
|
||||
.p-inputswitch {
|
||||
width: 2.0rem;
|
||||
height: 1.15rem;
|
||||
|
||||
.p-inputswitch-slider:before {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
left: 0.14rem;
|
||||
margin-top: -0.385rem;
|
||||
}
|
||||
|
||||
&.p-highlight .p-inputswitch-slider:before {
|
||||
transform: translateX(0.8rem);
|
||||
}
|
||||
|
||||
&:not(.p-disabled):has(.p-inputswitch-input:hover) .p-inputswitch-slider {
|
||||
background: rgb(255 255 255 / 21%);
|
||||
}
|
||||
|
||||
&.p-highlight .p-inputswitch-slider {
|
||||
background: #966d3d;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { MapUnionTypes } from '@/hooks/Mapper/types';
|
||||
import { MapUnionTypes, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
|
||||
export type MapData = MapUnionTypes & {
|
||||
@@ -30,10 +30,14 @@ const INITIAL_DATA: MapData = {
|
||||
isConnecting: false,
|
||||
connections: [],
|
||||
hoverNodeId: null,
|
||||
linkedSigEveId: '',
|
||||
visibleNodes: new Set(),
|
||||
showKSpaceBG: false,
|
||||
isThickConnections: false,
|
||||
userPermissions: {},
|
||||
systemSignatures: {} as Record<string, SystemSignature[]>,
|
||||
options: {} as Record<string, string | boolean>,
|
||||
is_subscription_active: false,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,4 +15,8 @@
|
||||
font-weight: bolder;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > .Eol {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
|
||||
import { k162Types } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
@@ -22,17 +22,22 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
const whData = useMemo(() => wormholesData[signature.type], [signature.type, wormholesData]);
|
||||
const whClass = useMemo(() => (whData ? WORMHOLES_ADDITIONAL_INFO[whData.dest] : null), [whData]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!signature.custom_info) {
|
||||
return null;
|
||||
}
|
||||
const customInfo = JSON.parse(signature.custom_info);
|
||||
if (!customInfo.k162Type) {
|
||||
return null;
|
||||
}
|
||||
return k162Types.find(x => x.value === customInfo.k162Type);
|
||||
const customInfo = useMemo(() => {
|
||||
return parseSignatureCustomInfo(signature.custom_info);
|
||||
}, [signature]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!customInfo?.k162Type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return K162_TYPES_MAP[customInfo.k162Type];
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.isEOL;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
if (signature.type === 'K162' && k162TypeOption) {
|
||||
const k162Data = wormholesData[k162TypeOption.whClassName];
|
||||
@@ -45,19 +50,19 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
return (
|
||||
<WdTooltipWrapper
|
||||
className={clsx(classes.Signature)}
|
||||
// @ts-ignore
|
||||
content={
|
||||
(
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
) as React.ReactNode
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="4" viewBox="0 0 13 4" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
|
||||
</svg>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
@@ -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,5 +1,3 @@
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useMemo } from 'react';
|
||||
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
@@ -21,5 +19,12 @@ export const MapInterface = () => {
|
||||
.filter(x => windowsSettings.visible.some(j => x.id === j));
|
||||
}, [windowsSettings]);
|
||||
|
||||
return <WindowManager windows={items} dragSelector=".react-grid-dragHandleExample" onChange={updateWidgetSettings} />;
|
||||
return (
|
||||
<WindowManager
|
||||
windows={items}
|
||||
viewPort={windowsSettings.viewPort}
|
||||
dragSelector=".react-grid-dragHandleExample"
|
||||
onChange={updateWidgetSettings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SystemInfo,
|
||||
SystemSignatures,
|
||||
SystemStructures,
|
||||
SystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
export const CURRENT_WINDOWS_VERSION = 8;
|
||||
@@ -16,6 +17,7 @@ export enum WidgetsIds {
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
structures = 'structures',
|
||||
kills = 'kills',
|
||||
}
|
||||
|
||||
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
|
||||
@@ -61,6 +63,13 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
zIndex: 0,
|
||||
content: () => <SystemStructures />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.kills,
|
||||
position: { x: 270, y: 730 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemKills />,
|
||||
},
|
||||
];
|
||||
|
||||
type WidgetsCheckboxesType = {
|
||||
@@ -89,4 +98,8 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
id: WidgetsIds.structures,
|
||||
label: 'Structures',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.kills,
|
||||
label: 'Kills',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,33 +61,60 @@ export const LocalCharacters = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(ref, 145);
|
||||
|
||||
const itemTemplate = useLocalCharactersItemTemplate(settings.showShipName);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<span className="select-none">Local{showList ? ` [${sorted.length}]` : ''}</span>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label={compact ? '' : 'Show offline'}
|
||||
value={settings.showOffline}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={() => setSettings(() => ({ ...settings, showOffline: !settings.showOffline }))}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
<div className="flex w-full items-center" ref={ref}>
|
||||
<div className="flex-shrink-0 select-none mr-2">
|
||||
Local{showList ? ` [${sorted.length}]` : ''}
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden">
|
||||
<LayoutEventBlocker className="flex items-center gap-2 justify-end">
|
||||
{showOffline && (
|
||||
<WdTooltipWrapper content="Show offline characters in system">
|
||||
<div className={clsx("min-w-0", { "max-w-[100px]": compact })}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show offline"
|
||||
value={settings.showOffline}
|
||||
classNameLabel={clsx("whitespace-nowrap", { "truncate": compact })}
|
||||
onChange={() =>
|
||||
setSettings(prev => ({ ...prev, showOffline: !prev.showOffline }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={clsx('w-4 h-4 cursor-pointer', {
|
||||
['hero-bars-2']: settings.compact,
|
||||
['hero-bars-3']: !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings(() => ({ ...settings, compact: !settings.compact }))}
|
||||
></span>
|
||||
</LayoutEventBlocker>
|
||||
{settings.compact && (
|
||||
<WdTooltipWrapper content="Show ship name in compact rows">
|
||||
<div className={clsx("min-w-0", { "max-w-[100px]": compact })}>
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show ship name"
|
||||
value={settings.showShipName}
|
||||
classNameLabel={clsx("whitespace-nowrap", { "truncate": compact })}
|
||||
onChange={() =>
|
||||
setSettings(prev => ({ ...prev, showShipName: !prev.showShipName }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={clsx("w-4 h-4 cursor-pointer", {
|
||||
"hero-bars-2": settings.compact,
|
||||
"hero-bars-3": !settings.compact,
|
||||
})}
|
||||
onClick={() => setSettings(prev => ({ ...prev, compact: !prev.compact }))}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -140,15 +131,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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SystemKillsContent } from './SystemKillsContent/SystemKillsContent';
|
||||
import { KillsHeader } from './components/SystemKillsHeader';
|
||||
import { useKillsWidgetSettings } from './hooks/useKillsWidgetSettings';
|
||||
import { useSystemKills } from './hooks/useSystemKills';
|
||||
import { KillsSettingsDialog } from './components/SystemKillsSettingsDialog';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
|
||||
export const SystemKills: React.FC = () => {
|
||||
const {
|
||||
data: { selectedSystems, systems, isSubscriptionActive },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const [systemId] = selectedSystems || [];
|
||||
|
||||
const [settingsDialogVisible, setSettingsDialogVisible] = useState(false);
|
||||
|
||||
const systemNameMap = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
systems.forEach(sys => {
|
||||
map[sys.id] = sys.temporary_name || sys.name || '???';
|
||||
});
|
||||
return map;
|
||||
}, [systems]);
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const visible = settings.showAll;
|
||||
|
||||
const { kills, isLoading, error } = useSystemKills({
|
||||
systemId,
|
||||
outCommand,
|
||||
showAllVisible: visible,
|
||||
});
|
||||
|
||||
const isNothingSelected = !systemId && !visible;
|
||||
const showLoading = isLoading && kills.length === 0;
|
||||
|
||||
const filteredKills = useMemo(() => {
|
||||
if (!settings.whOnly || !visible) return kills;
|
||||
return kills.filter(kill => {
|
||||
const system = systems.find(sys => sys.system_static_info.solar_system_id === kill.solar_system_id);
|
||||
if (!system) {
|
||||
console.warn(`System with id ${kill.solar_system_id} not found.`);
|
||||
return false;
|
||||
}
|
||||
return isWormholeSpace(system.system_static_info.system_class);
|
||||
});
|
||||
}, [kills, settings.whOnly, systems]);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<Widget
|
||||
label={
|
||||
<KillsHeader systemId={systemId} onOpenSettings={() => setSettingsDialogVisible(true)} />
|
||||
}
|
||||
>
|
||||
{!isSubscriptionActive && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
Kills available with 'Active' map subscription only (contact map administrators)
|
||||
</div>
|
||||
)}
|
||||
{isSubscriptionActive && (
|
||||
<>
|
||||
{isNothingSelected && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
No system selected (or toggle “Show all systems”)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && showLoading && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
Loading Kills...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && error && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected &&
|
||||
!showLoading &&
|
||||
!error &&
|
||||
(!filteredKills || filteredKills.length === 0) && (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
No kills found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isNothingSelected && !showLoading && !error && (
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SystemKillsContent
|
||||
key={settings.compact ? 'compact' : 'normal'}
|
||||
kills={filteredKills}
|
||||
systemNameMap={systemNameMap}
|
||||
compact={settings.compact}
|
||||
onlyOneSystem={!visible}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Widget>
|
||||
</div>
|
||||
|
||||
<KillsSettingsDialog visible={settingsDialogVisible} setVisible={setSettingsDialogVisible} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
.TableRowCompact {
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.Tooltip {
|
||||
white-space: pre-line;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { KillRow } from '../components/SystemKillsRow';
|
||||
|
||||
interface SystemKillsContentProps {
|
||||
kills: DetailedKill[];
|
||||
systemNameMap: Record<string, string>;
|
||||
compact?: boolean;
|
||||
onlyOneSystem?: boolean;
|
||||
}
|
||||
|
||||
export const SystemKillsContent: React.FC<SystemKillsContentProps> = ({
|
||||
kills,
|
||||
systemNameMap,
|
||||
compact = false,
|
||||
onlyOneSystem = false,
|
||||
}) => {
|
||||
const sortedKills = useMemo(() => {
|
||||
return [...kills].sort((a, b) => {
|
||||
const timeA = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||
const timeB = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
}, [kills]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col w-full text-stone-200 text-xs transition-all duration-300',
|
||||
compact ? 'p-1' : 'p-1'
|
||||
)}
|
||||
>
|
||||
{sortedKills.map(kill => {
|
||||
const systemIdStr = String(kill.solar_system_id);
|
||||
const systemName = systemNameMap[systemIdStr] || `System ${systemIdStr}`;
|
||||
|
||||
return (
|
||||
<KillRow
|
||||
key={kill.killmail_id}
|
||||
killDetails={kill}
|
||||
systemName={systemName}
|
||||
isCompact={compact}
|
||||
onlyOneSystem={onlyOneSystem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
|
||||
interface AttackerRowSubInfoProps {
|
||||
finalBlowCharId: number | null | undefined;
|
||||
finalBlowCharName?: string;
|
||||
attackerPortraitUrl: string | null;
|
||||
|
||||
finalBlowCorpId: number | null | undefined;
|
||||
finalBlowCorpName?: string;
|
||||
attackerCorpLogoUrl: string | null;
|
||||
|
||||
finalBlowAllianceId: number | null | undefined;
|
||||
finalBlowAllianceName?: string;
|
||||
attackerAllianceLogoUrl: string | null;
|
||||
|
||||
containerHeight?: number;
|
||||
}
|
||||
|
||||
export const AttackerRowSubInfo: React.FC<AttackerRowSubInfoProps> = ({
|
||||
finalBlowCharId = 0,
|
||||
finalBlowCharName,
|
||||
attackerPortraitUrl,
|
||||
containerHeight = 8,
|
||||
}) => {
|
||||
if (!attackerPortraitUrl || finalBlowCharId === null || finalBlowCharId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerClass = `h-${containerHeight}`;
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-start gap-1', containerClass)}>
|
||||
<div className="relative shrink-0 w-auto h-full overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', finalBlowCharId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt={finalBlowCharName || 'AttackerPortrait'}
|
||||
className={clsx(classes.killRowImage, 'h-full w-auto object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import {
|
||||
formatISK,
|
||||
formatTimeMixed,
|
||||
zkillLink,
|
||||
getAttackerSubscript,
|
||||
buildVictimImageUrls,
|
||||
buildAttackerImageUrls,
|
||||
getPrimaryLogoAndTooltip,
|
||||
getAttackerPrimaryImageAndTooltip,
|
||||
} from '../helpers';
|
||||
import { WdTooltipWrapper } from '../../../../ui-kit/WdTooltipWrapper';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export interface CompactKillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
|
||||
export const CompactKillRow: React.FC<CompactKillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
onlyOneSystem,
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
|
||||
// Victim
|
||||
victim_char_name = 'Unknown Pilot',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
victim_ship_name = 'Unknown Ship',
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
victim_char_id = 0,
|
||||
victim_corp_id = 0,
|
||||
victim_alliance_id = 0,
|
||||
victim_ship_type_id = 0,
|
||||
|
||||
// Attacker
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
final_blow_alliance_name = '',
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_corp_ticker = '',
|
||||
final_blow_corp_id = 0,
|
||||
final_blow_corp_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
|
||||
kill_time = '',
|
||||
total_value = 0,
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
|
||||
// Tickers & strings
|
||||
const victimAffiliationTicker =
|
||||
victim_alliance_ticker || victim_corp_ticker || 'No Ticker';
|
||||
const killValueFormatted =
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const attackerName = attackerIsNpc ? '' : final_blow_char_name;
|
||||
const attackerTicker = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
// Victim images, including the ship
|
||||
const {
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
victimShipUrl,
|
||||
} = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
|
||||
// Attacker corp/alliance
|
||||
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
// Victim corp/alliance logo
|
||||
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim'
|
||||
);
|
||||
|
||||
// Attacker corp/alliance or NPC ship
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center border-b border-stone-800',
|
||||
'text-xs whitespace-nowrap overflow-hidden leading-none'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-8 h-8 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{victimPrimaryLogoUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryLogoUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col ml-2 min-w-0 overflow-hidden leading-[1rem]">
|
||||
<div className="truncate text-stone-200">
|
||||
{victim_char_name}
|
||||
<span className="text-stone-400"> / {victimAffiliationTicker}</span>
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">{killValueFormatted}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center ml-auto gap-2">
|
||||
<div className="flex flex-col items-end min-w-0 overflow-hidden text-right leading-[1rem]">
|
||||
{!attackerIsNpc && (attackerName || attackerTicker) && (
|
||||
<div className="truncate text-stone-200">
|
||||
{attackerName}
|
||||
{attackerTicker && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerTicker}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName ? (
|
||||
<>
|
||||
{systemName} /{' '}
|
||||
<span className="ml-1 text-red-400">{killTimeAgo}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-red-400">{killTimeAgo}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative block shrink-0 w-8 h-8 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(
|
||||
classes.killRowImage,
|
||||
'w-full h-full object-contain'
|
||||
)}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
classes.attackerCountLabel,
|
||||
attackerSubscript.cssClass,
|
||||
'text-[0.6rem] leading-none px-[2px]'
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,263 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import {
|
||||
formatISK,
|
||||
formatTimeMixed,
|
||||
zkillLink,
|
||||
getAttackerSubscript,
|
||||
buildVictimImageUrls,
|
||||
buildAttackerImageUrls,
|
||||
getPrimaryLogoAndTooltip,
|
||||
getAttackerPrimaryImageAndTooltip,
|
||||
} from '../helpers';
|
||||
import { VictimRowSubInfo } from './VictimRowSubInfo';
|
||||
import { WdTooltipWrapper } from '../../../../ui-kit/WdTooltipWrapper';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export interface FullKillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
onlyOneSystem: boolean;
|
||||
}
|
||||
|
||||
export const FullKillRow: React.FC<FullKillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
onlyOneSystem,
|
||||
}) => {
|
||||
const {
|
||||
killmail_id = 0,
|
||||
|
||||
// Victim
|
||||
victim_char_name = '',
|
||||
victim_alliance_ticker = '',
|
||||
victim_corp_ticker = '',
|
||||
victim_ship_name = '',
|
||||
victim_char_id = 0,
|
||||
victim_corp_id = 0,
|
||||
victim_alliance_id = 0,
|
||||
victim_ship_type_id = 0,
|
||||
victim_corp_name = '',
|
||||
victim_alliance_name = '',
|
||||
|
||||
// Attacker
|
||||
final_blow_char_id = 0,
|
||||
final_blow_char_name = '',
|
||||
final_blow_alliance_ticker = '',
|
||||
final_blow_corp_ticker = '',
|
||||
final_blow_corp_name = '',
|
||||
final_blow_alliance_name = '',
|
||||
final_blow_corp_id = 0,
|
||||
final_blow_alliance_id = 0,
|
||||
final_blow_ship_name = '',
|
||||
final_blow_ship_type_id = 0,
|
||||
|
||||
total_value = 0,
|
||||
kill_time = '',
|
||||
} = killDetails || {};
|
||||
|
||||
const attackerIsNpc = final_blow_char_id === 0;
|
||||
const victimAffiliation =
|
||||
victim_alliance_ticker || victim_corp_ticker || null;
|
||||
const attackerAffiliation = attackerIsNpc
|
||||
? ''
|
||||
: final_blow_alliance_ticker || final_blow_corp_ticker || '';
|
||||
|
||||
const killValueFormatted =
|
||||
total_value != null && total_value > 0 ? `${formatISK(total_value)} ISK` : null;
|
||||
const killTimeAgo = kill_time ? formatTimeMixed(kill_time) : '0h ago';
|
||||
|
||||
// Victim images, now also pulling victimShipUrl
|
||||
const {
|
||||
victimPortraitUrl,
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
victimShipUrl,
|
||||
} = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
// Attacker images
|
||||
const {
|
||||
attackerPortraitUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerAllianceLogoUrl,
|
||||
} = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
// Primary corp/alliance logo for victim
|
||||
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } =
|
||||
getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim'
|
||||
);
|
||||
|
||||
// Primary image for attacker => NPC => ship, else corp/alliance
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } =
|
||||
getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id
|
||||
);
|
||||
|
||||
const attackerSubscript = getAttackerSubscript(killDetails);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.killRowContainer,
|
||||
'h-18 w-full justify-between items-start text-sm py-[4px]'
|
||||
)}
|
||||
>
|
||||
{/* ---------------- Victim Side ---------------- */}
|
||||
<div className="flex items-start gap-1 min-w-0 h-full">
|
||||
{victimShipUrl && (
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimShipUrl}
|
||||
alt="VictimShip"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{victimPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={victimPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPrimaryImageUrl}
|
||||
alt="VictimPrimaryLogo"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
<VictimRowSubInfo
|
||||
victimCharName={victim_char_name}
|
||||
victimCharacterId={victim_char_id}
|
||||
victimPortraitUrl={victimPortraitUrl}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col text-stone-200 leading-4 min-w-0 overflow-hidden">
|
||||
<div className="truncate">
|
||||
<span className="font-semibold">{victim_char_name}</span>
|
||||
{victimAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {victimAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-300">
|
||||
{victim_ship_name}
|
||||
{killValueFormatted && (
|
||||
<>
|
||||
<span className="ml-1 text-stone-400">/</span>
|
||||
<span className="ml-1 text-green-400">
|
||||
{killValueFormatted}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-stone-400">
|
||||
{!onlyOneSystem && systemName && <span>{systemName}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-1 min-w-0 h-full">
|
||||
<div className="flex flex-col items-end leading-4 min-w-0 overflow-hidden text-right">
|
||||
{!attackerIsNpc && (
|
||||
<div className="truncate font-semibold">
|
||||
{final_blow_char_name}
|
||||
{attackerAffiliation && (
|
||||
<span className="ml-1 text-stone-400">/ {attackerAffiliation}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!attackerIsNpc && final_blow_ship_name && (
|
||||
<div className="truncate text-stone-300">{final_blow_ship_name}</div>
|
||||
)}
|
||||
<div className="truncate text-red-400">{killTimeAgo}</div>
|
||||
</div>
|
||||
|
||||
{!attackerIsNpc && attackerPortraitUrl && final_blow_char_id && final_blow_char_id > 0 && (
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', final_blow_char_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPortraitUrl}
|
||||
alt="AttackerPortrait"
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{attackerPrimaryImageUrl && (
|
||||
<WdTooltipWrapper
|
||||
content={attackerPrimaryTooltip}
|
||||
position={TooltipPosition.top}
|
||||
>
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={attackerPrimaryImageUrl}
|
||||
alt={attackerIsNpc ? 'NpcShip' : 'AttackerPrimaryLogo'}
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
{attackerSubscript && (
|
||||
<span
|
||||
className={clsx(
|
||||
attackerSubscript.cssClass,
|
||||
classes.attackerCountLabel
|
||||
)}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.killRowContainer {
|
||||
@apply flex items-center whitespace-nowrap overflow-hidden;
|
||||
&:not(:last-child) {
|
||||
@apply border-b border-stone-800;
|
||||
}
|
||||
@apply bg-transparent transition-all hover:bg-stone-900 hover:border-stone-700;
|
||||
}
|
||||
|
||||
.killRowImage {
|
||||
@apply border border-stone-800 rounded-[4px] object-contain;
|
||||
}
|
||||
|
||||
.attackerCountLabel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 10px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.attackerCountLabelCompact {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 1px 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
WdCheckbox,
|
||||
WdImgButton,
|
||||
TooltipPosition,
|
||||
SystemView,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
interface KillsWidgetHeaderProps {
|
||||
systemId?: string;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
export const KillsHeader: React.FC<KillsWidgetHeaderProps> = ({ systemId, onOpenSettings }) => {
|
||||
const [settings, setSettings] = useKillsWidgetSettings();
|
||||
const { showAll } = settings;
|
||||
|
||||
const onToggleShowAllVisible = () => {
|
||||
setSettings(prev => ({ ...prev, showAll: !prev.showAll }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center text-xs w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-stone-400">
|
||||
Kills
|
||||
{systemId && !showAll && ' in '}
|
||||
</div>
|
||||
{systemId && !showAll && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2 items-center">
|
||||
<WdCheckbox
|
||||
size="xs"
|
||||
labelSide="left"
|
||||
label="Show all systems"
|
||||
value={showAll}
|
||||
classNameLabel="text-stone-400 hover:text-stone-200 transition duration-300"
|
||||
onChange={onToggleShowAllVisible}
|
||||
/>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.SLIDERS_H}
|
||||
onClick={onOpenSettings}
|
||||
tooltip={{
|
||||
content: 'Open Kills Settings',
|
||||
position: TooltipPosition.left,
|
||||
}}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { CompactKillRow } from './CompactKillRow';
|
||||
import { FullKillRow } from './FullKillRow';
|
||||
|
||||
export interface KillRowProps {
|
||||
killDetails: DetailedKill;
|
||||
systemName: string;
|
||||
isCompact?: boolean;
|
||||
onlyOneSystem?: boolean;
|
||||
}
|
||||
|
||||
export const KillRow: React.FC<KillRowProps> = ({
|
||||
killDetails,
|
||||
systemName,
|
||||
isCompact = false,
|
||||
onlyOneSystem = false,
|
||||
}) => {
|
||||
if (isCompact) {
|
||||
return <CompactKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
|
||||
}
|
||||
|
||||
return <FullKillRow killDetails={killDetails} systemName={systemName} onlyOneSystem={onlyOneSystem} />;
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { WdImgButton, SystemView, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useKillsWidgetSettings } from '../hooks/useKillsWidgetSettings';
|
||||
import {
|
||||
AddSystemDialog,
|
||||
SearchOnSubmitCallback,
|
||||
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
|
||||
|
||||
interface KillsSettingsDialogProps {
|
||||
visible: boolean;
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export const KillsSettingsDialog: React.FC<KillsSettingsDialogProps> = ({ visible, setVisible }) => {
|
||||
const [globalSettings, setGlobalSettings] = useKillsWidgetSettings();
|
||||
const localRef = useRef({
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
});
|
||||
|
||||
const [, forceRender] = useState(0);
|
||||
|
||||
const [addSystemDialogVisible, setAddSystemDialogVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
localRef.current = {
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
whOnly: globalSettings.whOnly,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}
|
||||
}, [visible, globalSettings]);
|
||||
|
||||
const handleCompactChange = useCallback((checked: boolean) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
compact: checked,
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleWHChange = useCallback((checked: boolean) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
whOnly: checked,
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleRemoveSystem = useCallback((sysId: number) => {
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
excludedSystems: localRef.current.excludedSystems.filter(id => id !== sysId),
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleAddSystemSubmit: SearchOnSubmitCallback = useCallback(item => {
|
||||
if (localRef.current.excludedSystems.includes(item.value)) {
|
||||
return;
|
||||
}
|
||||
localRef.current = {
|
||||
...localRef.current,
|
||||
excludedSystems: [...localRef.current.excludedSystems, item.value],
|
||||
};
|
||||
forceRender(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
setGlobalSettings(prev => ({
|
||||
...prev,
|
||||
...localRef.current,
|
||||
}));
|
||||
setVisible(false);
|
||||
}, [setGlobalSettings, setVisible]);
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, [setVisible]);
|
||||
|
||||
const localData = localRef.current;
|
||||
const excluded = localData.excludedSystems || [];
|
||||
|
||||
return (
|
||||
<Dialog header="Kills Settings" visible={visible} style={{ width: '440px' }} draggable={false} onHide={handleHide}>
|
||||
<div className="flex flex-col gap-3 p-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="kills-compact-mode"
|
||||
checked={localData.compact}
|
||||
onChange={e => handleCompactChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="kills-compact-mode" className="cursor-pointer">
|
||||
Use compact mode
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="kills-wormhole-only-mode"
|
||||
checked={localData.whOnly}
|
||||
onChange={e => handleWHChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="kills-wh-only-mode" className="cursor-pointer">
|
||||
Only show wormhole kills
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm text-stone-400">Excluded Systems</label>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={() => setAddSystemDialogVisible(true)}
|
||||
tooltip={{ content: 'Add system to excluded list' }}
|
||||
/>
|
||||
</div>
|
||||
{excluded.length === 0 && <div className="text-stone-500 text-xs italic">No systems excluded.</div>}
|
||||
{excluded.map(sysId => (
|
||||
<div key={sysId} className="flex items-center justify-between border-b border-stone-600 py-1 px-1 text-xs">
|
||||
<SystemView systemId={sysId.toString()} hideRegion compact/>
|
||||
|
||||
<WdImgButton
|
||||
className={PrimeIcons.TRASH}
|
||||
onClick={() => handleRemoveSystem(sysId)}
|
||||
tooltip={{ content: 'Remove from excluded', position: TooltipPosition.top }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button onClick={handleApply} label="Apply" outlined size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddSystemDialog
|
||||
title="Add system to kills exclude list"
|
||||
visible={addSystemDialogVisible}
|
||||
setVisible={() => setAddSystemDialogVisible(false)}
|
||||
onSubmit={handleAddSystemSubmit}
|
||||
excludedSystems={excluded}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// VictimSubRowInfo.tsx
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { zkillLink } from '../helpers';
|
||||
import classes from './SystemKillRow.module.scss';
|
||||
|
||||
interface VictimRowSubInfoProps {
|
||||
victimCharacterId: number | null;
|
||||
victimPortraitUrl: string | null;
|
||||
victimCharName?: string;
|
||||
}
|
||||
|
||||
export const VictimRowSubInfo: React.FC<VictimRowSubInfoProps> = ({
|
||||
victimCharacterId = 0,
|
||||
victimPortraitUrl,
|
||||
victimCharName,
|
||||
}) => {
|
||||
if (!victimPortraitUrl || victimCharacterId === null || victimCharacterId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-1 h-14">
|
||||
<div className="relative shrink-0 w-14 h-14 overflow-hidden">
|
||||
<a
|
||||
href={zkillLink('character', victimCharacterId)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={victimPortraitUrl}
|
||||
alt={victimCharName || 'Victim Portrait'}
|
||||
className={clsx(classes.killRowImage, 'w-full h-full object-contain')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './linkHelpers';
|
||||
export * from './killRowUtils';
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
|
||||
/** Returns "5m ago", "3h ago", "2.5d ago", etc. */
|
||||
export function formatTimeMixed(killTime: string): string {
|
||||
const killDate = new Date(killTime);
|
||||
const diffMs = Date.now() - killDate.getTime();
|
||||
const diffHours = diffMs / (1000 * 60 * 60);
|
||||
|
||||
if (diffHours < 1) {
|
||||
const mins = Math.round(diffHours * 60);
|
||||
return `${mins}m ago`;
|
||||
} else if (diffHours < 24) {
|
||||
const hours = Math.round(diffHours);
|
||||
return `${hours}h ago`;
|
||||
} else {
|
||||
const days = diffHours / 24;
|
||||
const roundedDays = days.toFixed(1);
|
||||
return `${roundedDays}d ago`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Formats integer ISK values into k/M/B/T. */
|
||||
export function formatISK(value: number): string {
|
||||
if (value >= 1_000_000_000_000) {
|
||||
return `${(value / 1_000_000_000_000).toFixed(2)}T`;
|
||||
} else if (value >= 1_000_000_000) {
|
||||
return `${(value / 1_000_000_000).toFixed(2)}B`;
|
||||
} else if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(2)}M`;
|
||||
} else if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(2)}k`;
|
||||
}
|
||||
return Math.round(value).toString();
|
||||
}
|
||||
|
||||
export function getAttackerSubscript(kill: DetailedKill) {
|
||||
if (kill.npc) {
|
||||
return { label: 'npc', cssClass: 'text-purple-400' };
|
||||
}
|
||||
const count = kill.attacker_count ?? 0;
|
||||
if (count === 1) {
|
||||
return { label: 'solo', cssClass: 'text-green-400' };
|
||||
} else if (count > 1) {
|
||||
return { label: String(count), cssClass: 'text-white' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
const ZKILL_URL = 'https://zkillboard.com';
|
||||
const BASE_IMAGE_URL = 'https://images.evetech.net';
|
||||
|
||||
export function zkillLink(type: 'kill' | 'character' | 'corporation' | 'alliance', id?: number | null): string {
|
||||
if (!id) return `${ZKILL_URL}`;
|
||||
if (type === 'kill') return `${ZKILL_URL}/kill/${id}/`;
|
||||
if (type === 'character') return `${ZKILL_URL}/character/${id}/`;
|
||||
if (type === 'corporation') return `${ZKILL_URL}/corporation/${id}/`;
|
||||
if (type === 'alliance') return `${ZKILL_URL}/alliance/${id}/`;
|
||||
return `${ZKILL_URL}`;
|
||||
}
|
||||
|
||||
export function eveImageUrl(
|
||||
category: 'characters' | 'corporations' | 'alliances' | 'types',
|
||||
id?: number | null,
|
||||
variation: string = 'icon',
|
||||
size?: number,
|
||||
): string | null {
|
||||
if (!id || id <= 0) {
|
||||
return null;
|
||||
}
|
||||
let url = `${BASE_IMAGE_URL}/${category}/${id}/${variation}`;
|
||||
if (size) {
|
||||
url += `?size=${size}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function buildVictimImageUrls(args: {
|
||||
victim_char_id?: number | null;
|
||||
victim_ship_type_id?: number | null;
|
||||
victim_corp_id?: number | null;
|
||||
victim_alliance_id?: number | null;
|
||||
}) {
|
||||
const { victim_char_id, victim_ship_type_id, victim_corp_id, victim_alliance_id } = args;
|
||||
|
||||
const victimPortraitUrl = eveImageUrl('characters', victim_char_id, 'portrait', 64);
|
||||
const victimShipUrl = eveImageUrl('types', victim_ship_type_id, 'render', 64);
|
||||
const victimCorpLogoUrl = eveImageUrl('corporations', victim_corp_id, 'logo', 32);
|
||||
const victimAllianceLogoUrl = eveImageUrl('alliances', victim_alliance_id, 'logo', 32);
|
||||
|
||||
return {
|
||||
victimPortraitUrl,
|
||||
victimShipUrl,
|
||||
victimCorpLogoUrl,
|
||||
victimAllianceLogoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAttackerShipUrl(final_blow_ship_type_id?: number | null): string | null {
|
||||
return eveImageUrl('types', final_blow_ship_type_id, 'render', 64);
|
||||
}
|
||||
|
||||
export function buildAttackerImageUrls(args: {
|
||||
final_blow_char_id?: number | null;
|
||||
final_blow_corp_id?: number | null;
|
||||
final_blow_alliance_id?: number | null;
|
||||
}) {
|
||||
const { final_blow_char_id, final_blow_corp_id, final_blow_alliance_id } = args;
|
||||
|
||||
const attackerPortraitUrl = eveImageUrl('characters', final_blow_char_id, 'portrait', 64);
|
||||
const attackerCorpLogoUrl = eveImageUrl('corporations', final_blow_corp_id, 'logo', 32);
|
||||
const attackerAllianceLogoUrl = eveImageUrl('alliances', final_blow_alliance_id, 'logo', 32);
|
||||
|
||||
return {
|
||||
attackerPortraitUrl,
|
||||
attackerCorpLogoUrl,
|
||||
attackerAllianceLogoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPrimaryLogoAndTooltip(
|
||||
allianceUrl: string | null,
|
||||
corpUrl: string | null,
|
||||
allianceName: string,
|
||||
corpName: string,
|
||||
fallback: string,
|
||||
) {
|
||||
let url: string | null = null;
|
||||
let tooltip = '';
|
||||
|
||||
if (allianceUrl) {
|
||||
url = allianceUrl;
|
||||
tooltip = allianceName || fallback;
|
||||
} else if (corpUrl) {
|
||||
url = corpUrl;
|
||||
tooltip = corpName || fallback;
|
||||
}
|
||||
|
||||
return { url, tooltip };
|
||||
}
|
||||
|
||||
export function getAttackerPrimaryImageAndTooltip(
|
||||
isNpc: boolean,
|
||||
allianceUrl: string | null,
|
||||
corpUrl: string | null,
|
||||
allianceName: string,
|
||||
corpName: string,
|
||||
finalBlowShipTypeId: number | null,
|
||||
npcFallback: string = 'NPC Attacker',
|
||||
) {
|
||||
if (isNpc) {
|
||||
const shipUrl = buildAttackerShipUrl(finalBlowShipTypeId);
|
||||
return {
|
||||
url: shipUrl,
|
||||
tooltip: npcFallback,
|
||||
};
|
||||
}
|
||||
|
||||
return getPrimaryLogoAndTooltip(allianceUrl, corpUrl, allianceName, corpName, 'Attacker');
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
export interface KillsWidgetSettings {
|
||||
compact: boolean;
|
||||
showAll: boolean;
|
||||
whOnly: boolean;
|
||||
excludedSystems: number[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
||||
compact: true,
|
||||
showAll: false,
|
||||
whOnly: true,
|
||||
excludedSystems: [],
|
||||
version: 0,
|
||||
};
|
||||
|
||||
function mergeWithDefaults(settings?: Partial<KillsWidgetSettings>): KillsWidgetSettings {
|
||||
if (!settings) {
|
||||
return DEFAULT_KILLS_WIDGET_SETTINGS;
|
||||
}
|
||||
|
||||
return {
|
||||
...DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
...settings,
|
||||
excludedSystems: Array.isArray(settings.excludedSystems) ? settings.excludedSystems : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function useKillsWidgetSettings() {
|
||||
const [rawValue, setRawValue] = useLocalStorageState<KillsWidgetSettings | undefined>('kills:widget:settings');
|
||||
|
||||
const value = useMemo<KillsWidgetSettings>(() => {
|
||||
return mergeWithDefaults(rawValue);
|
||||
}, [rawValue]);
|
||||
|
||||
const setValue = useCallback(
|
||||
(newVal: KillsWidgetSettings | ((prev: KillsWidgetSettings) => KillsWidgetSettings)) => {
|
||||
setRawValue(prev => {
|
||||
const mergedPrev = mergeWithDefaults(prev);
|
||||
|
||||
const nextUnmerged = typeof newVal === 'function' ? newVal(mergedPrev) : newVal;
|
||||
|
||||
return mergeWithDefaults(nextUnmerged);
|
||||
});
|
||||
},
|
||||
[setRawValue],
|
||||
);
|
||||
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useKillsWidgetSettings } from './useKillsWidgetSettings';
|
||||
|
||||
interface UseSystemKillsProps {
|
||||
systemId?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
outCommand: (payload: any) => Promise<any>;
|
||||
showAllVisible?: boolean;
|
||||
sinceHours?: number;
|
||||
}
|
||||
|
||||
function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] {
|
||||
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
|
||||
const byId: Record<string, DetailedKill> = {};
|
||||
|
||||
for (const kill of [...existing, ...incoming]) {
|
||||
if (!kill.kill_time) {
|
||||
continue;
|
||||
}
|
||||
const killTimeMs = new Date(kill.kill_time).valueOf();
|
||||
|
||||
if (killTimeMs >= cutoff) {
|
||||
byId[kill.killmail_id] = kill;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(byId);
|
||||
}
|
||||
|
||||
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
|
||||
const { data, update } = useMapRootState();
|
||||
const { detailedKills = {}, systems = [] } = data;
|
||||
|
||||
const [settings] = useKillsWidgetSettings();
|
||||
const excludedSystems = settings.excludedSystems;
|
||||
|
||||
// When showing all visible kills, filter out excluded systems;
|
||||
// when showAllVisible is false, ignore the exclusion filter.
|
||||
const effectiveSystemIds = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
}
|
||||
return systems.map(s => s.id);
|
||||
}, [systems, excludedSystems, showAllVisible]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0);
|
||||
|
||||
const mergeKillsIntoGlobal = useCallback(
|
||||
(killsMap: Record<string, DetailedKill[]>) => {
|
||||
update(prev => {
|
||||
const oldMap = prev.detailedKills ?? {};
|
||||
const updated: Record<string, DetailedKill[]> = { ...oldMap };
|
||||
|
||||
for (const [sid, newKills] of Object.entries(killsMap)) {
|
||||
const existing = updated[sid] ?? [];
|
||||
const combined = combineKills(existing, newKills, sinceHours);
|
||||
updated[sid] = combined;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
detailedKills: updated,
|
||||
};
|
||||
});
|
||||
},
|
||||
[update, sinceHours],
|
||||
);
|
||||
|
||||
const fetchKills = useCallback(
|
||||
async (forceFallback = false) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let eventType: OutCommand;
|
||||
let requestData: Record<string, unknown>;
|
||||
|
||||
if (showAllVisible || forceFallback) {
|
||||
eventType = OutCommand.getSystemsKills;
|
||||
requestData = {
|
||||
system_ids: effectiveSystemIds,
|
||||
since_hours: sinceHours,
|
||||
};
|
||||
} else if (systemId) {
|
||||
eventType = OutCommand.getSystemKills;
|
||||
requestData = {
|
||||
system_id: systemId,
|
||||
since_hours: sinceHours,
|
||||
};
|
||||
} else {
|
||||
// If there's no system and not showing all, do nothing
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await outCommand({
|
||||
type: eventType,
|
||||
data: requestData,
|
||||
});
|
||||
|
||||
// Single system => `resp.kills`
|
||||
if (resp?.kills) {
|
||||
const arr = resp.kills as DetailedKill[];
|
||||
const sid = systemId ?? 'unknown';
|
||||
mergeKillsIntoGlobal({ [sid]: arr });
|
||||
}
|
||||
// multiple systems => `resp.systems_kills`
|
||||
else if (resp?.systems_kills) {
|
||||
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
|
||||
} else {
|
||||
console.warn('[useSystemKills] Unexpected kills response =>', resp);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[useSystemKills] Failed to fetch kills:', err);
|
||||
setError(err instanceof Error ? err.message : 'Error fetching kills');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
|
||||
);
|
||||
|
||||
const debouncedFetchKills = useMemo(
|
||||
() =>
|
||||
debounce(fetchKills, 500, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
}),
|
||||
[fetchKills],
|
||||
);
|
||||
|
||||
const finalKills = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
} else if (systemId) {
|
||||
return detailedKills[systemId] ?? [];
|
||||
} else if (didFallbackFetch.current) {
|
||||
// if we already did a fallback, we may have data for multiple systems
|
||||
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
}
|
||||
return [];
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, detailedKills]);
|
||||
|
||||
const effectiveIsLoading = isLoading && finalKills.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!systemId && !showAllVisible && !didFallbackFetch.current) {
|
||||
didFallbackFetch.current = true;
|
||||
// Cancel any queued debounced calls, then do the fallback.
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(true); // forceFallback => fetch as though showAllVisible is true
|
||||
}
|
||||
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills]);
|
||||
|
||||
useEffect(() => {
|
||||
if (effectiveSystemIds.length === 0) return;
|
||||
|
||||
if (showAllVisible || systemId) {
|
||||
debouncedFetchKills();
|
||||
// Clean up the debounce on unmount or changes
|
||||
return () => debouncedFetchKills.cancel();
|
||||
}
|
||||
}, [showAllVisible, systemId, effectiveSystemIds, debouncedFetchKills]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
debouncedFetchKills.cancel();
|
||||
fetchKills(); // immediate (non-debounced) call
|
||||
}, [debouncedFetchKills, fetchKills]);
|
||||
|
||||
return {
|
||||
kills: finalKills,
|
||||
isLoading: effectiveIsLoading,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SystemKills';
|
||||
@@ -1,28 +1,29 @@
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SystemViewStandalone, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
import {
|
||||
k162Types,
|
||||
renderK162Type,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { renderName } from './renderName.tsx';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
|
||||
export const renderInfoColumn = (row: SystemSignature) => {
|
||||
if (!row.group || row.group === SignatureGroup.Wormhole) {
|
||||
let k162TypeOption = null;
|
||||
if (row.custom_info) {
|
||||
const customInfo = JSON.parse(row.custom_info);
|
||||
if (customInfo.k162Type) {
|
||||
k162TypeOption = k162Types.find(x => x.value === customInfo.k162Type);
|
||||
}
|
||||
}
|
||||
const customInfo = parseSignatureCustomInfo(row.custom_info);
|
||||
|
||||
const k162TypeOption = customInfo.k162Type ? K162_TYPES_MAP[customInfo.k162Type] : null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-center gap-[4px]">
|
||||
{customInfo.isEOL && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{row.type && (
|
||||
<WHClassView
|
||||
className="text-[11px]"
|
||||
@@ -34,7 +35,7 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!row.linked_system && row.type === 'K162' && !!k162TypeOption && <>{renderK162Type(k162TypeOption)}</>}
|
||||
{!row.linked_system && row.type === 'K162' && k162TypeOption && renderK162Type(k162TypeOption)}
|
||||
|
||||
{row.linked_system && (
|
||||
<>
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './SystemInfo';
|
||||
export * from './RoutesWidget';
|
||||
export * from './SystemSignatures';
|
||||
export * from './SystemStructures';
|
||||
export * from './SystemKills';
|
||||
|
||||
@@ -2,7 +2,9 @@ import clsx from 'clsx';
|
||||
import classes from './PassageCard.module.scss';
|
||||
import { Passage } from '@/hooks/Mapper/types';
|
||||
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type PassageCardType = {
|
||||
// compact?: boolean;
|
||||
@@ -26,6 +28,11 @@ export const getShipName = (name: string) => {
|
||||
export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardType) => {
|
||||
const isOwn = false;
|
||||
|
||||
const insertedAt = useMemo(() => {
|
||||
const date = new Date(inserted_at);
|
||||
return date.toLocaleString();
|
||||
}, [inserted_at]);
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
|
||||
<div className="flex flex-col justify-between px-2 py-1 gap-1">
|
||||
@@ -76,7 +83,9 @@ export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardT
|
||||
{/*time and class*/}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-stone-400">
|
||||
<TimeAgo timestamp={inserted_at} />
|
||||
<WdTooltipWrapper content={insertedAt}>
|
||||
<TimeAgo timestamp={inserted_at} />
|
||||
</WdTooltipWrapper>
|
||||
</span>
|
||||
|
||||
<div className="text-stone-400">{kgToTons(parseInt(ship.ship_type_info.mass))}</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styles from './MapSettings.module.scss';
|
||||
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface PrettySwitchboxProps {
|
||||
|
||||
@@ -53,6 +53,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
...out,
|
||||
custom_info: JSON.stringify({
|
||||
k162Type: values.k162Type,
|
||||
isEOL: values.isEOL,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -127,14 +128,17 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
const { linked_system, custom_info, ...rest } = signatureData;
|
||||
|
||||
let k162Type = null;
|
||||
let isEOL = false;
|
||||
if (custom_info) {
|
||||
const customInfo = JSON.parse(custom_info);
|
||||
k162Type = customInfo.k162Type;
|
||||
isEOL = customInfo.isEOL;
|
||||
}
|
||||
|
||||
signatureForm.reset({
|
||||
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
|
||||
k162Type: k162Type,
|
||||
isEOL: isEOL,
|
||||
...rest,
|
||||
});
|
||||
}, [signatureForm, signatureData]);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { InputSwitch } from 'primereact/inputswitch';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export interface SignatureEOLCheckboxProps {
|
||||
name: string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
export const SignatureEOLCheckbox = ({ name, defaultValue = false }: SignatureEOLCheckboxProps) => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
// @ts-ignore
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => {
|
||||
return <InputSwitch className="my-1" checked={!!field.value} onChange={e => field.onChange(e.value)} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SignatureEOLCheckbox.tsx';
|
||||
@@ -3,6 +3,7 @@ import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureWormholeTypeSelect';
|
||||
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
|
||||
import { SignatureEOLCheckbox } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureEOLCheckbox';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
const { watch } = useFormContext<SystemSignature>();
|
||||
@@ -26,6 +27,11 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<span>Leads To:</span>
|
||||
<SignatureLeadsToSelect name="linked_system" />
|
||||
</label>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>EOL:</span>
|
||||
<SignatureEOLCheckbox name="isEOL" />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,100 +3,8 @@ import clsx from 'clsx';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useMemo } from 'react';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const k162Types = [
|
||||
{
|
||||
label: 'Hi-Sec',
|
||||
value: 'hs',
|
||||
whClassName: 'A641',
|
||||
},
|
||||
{
|
||||
label: 'Low-Sec',
|
||||
value: 'ls',
|
||||
whClassName: 'J377',
|
||||
},
|
||||
{
|
||||
label: 'Null-Sec',
|
||||
value: 'ns',
|
||||
whClassName: 'C248',
|
||||
},
|
||||
{
|
||||
label: 'C1',
|
||||
value: 'c1',
|
||||
whClassName: 'E004',
|
||||
},
|
||||
{
|
||||
label: 'C2',
|
||||
value: 'c2',
|
||||
whClassName: 'D382',
|
||||
},
|
||||
{
|
||||
label: 'C3',
|
||||
value: 'c3',
|
||||
whClassName: 'L477',
|
||||
},
|
||||
{
|
||||
label: 'C4',
|
||||
value: 'c4',
|
||||
whClassName: 'M001',
|
||||
},
|
||||
{
|
||||
label: 'C5',
|
||||
value: 'c5',
|
||||
whClassName: 'L614',
|
||||
},
|
||||
{
|
||||
label: 'C6',
|
||||
value: 'c6',
|
||||
whClassName: 'G008',
|
||||
},
|
||||
{
|
||||
label: 'C13',
|
||||
value: 'c13',
|
||||
whClassName: 'A009',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
whClassName: 'F353',
|
||||
},
|
||||
{
|
||||
label: 'Pochven',
|
||||
value: 'pochven',
|
||||
whClassName: 'F216',
|
||||
},
|
||||
];
|
||||
|
||||
const renderNoValue = () => <div className="flex gap-2 items-center">-Unknown-</div>;
|
||||
|
||||
// @ts-ignore
|
||||
export const renderK162Type = (option: {
|
||||
label?: string;
|
||||
value: string;
|
||||
security?: string;
|
||||
system_class?: number;
|
||||
whClassName?: string;
|
||||
}) => {
|
||||
if (!option) {
|
||||
return renderNoValue();
|
||||
}
|
||||
const { value, whClassName = '' } = option;
|
||||
if (value == null) {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={whClassName}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
);
|
||||
};
|
||||
import { K162_TYPES } from '@/hooks/Mapper/constants.ts';
|
||||
import { renderK162Type } from '.';
|
||||
|
||||
export interface SignatureK162TypeSelectProps {
|
||||
name: string;
|
||||
@@ -107,7 +15,7 @@ export const SignatureK162TypeSelect = ({ name, defaultValue = '' }: SignatureK1
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [{ value: null }, ...k162Types];
|
||||
return [{ value: null }, ...K162_TYPES];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './SignatureK162TypeSelect.tsx';
|
||||
export * from './renderK162Type.tsx';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { K162Type } from '@/hooks/Mapper/constants.ts';
|
||||
|
||||
const renderNoValue = () => <div className="flex gap-2 items-center">-Unknown-</div>;
|
||||
|
||||
export const renderK162Type = (option: K162Type) => {
|
||||
if (!option) {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
const { value, whClassName = '' } = option;
|
||||
if (value == null) {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={whClassName}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,7 @@
|
||||
border-style: solid;
|
||||
border-color: #272727;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border-radius: 3px;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.CharName {
|
||||
@@ -26,7 +26,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.CharIcon {}
|
||||
.CharIcon {
|
||||
border-radius: 0 !important;
|
||||
border: 1px solid #2b2b2b;
|
||||
}
|
||||
|
||||
.CharRow {
|
||||
display: grid;
|
||||
|
||||
@@ -3,13 +3,13 @@ import clsx from 'clsx';
|
||||
import classes from './CharacterCard.module.scss';
|
||||
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
|
||||
import { CharacterTypeRaw, WithIsOwnCharacter } from '@/hooks/Mapper/types';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
|
||||
type CharacterCardProps = {
|
||||
compact?: boolean;
|
||||
showShipName?: boolean;
|
||||
showSystem?: boolean;
|
||||
showShipName?: boolean;
|
||||
useSystemsCache?: boolean;
|
||||
} & CharacterTypeRaw &
|
||||
WithIsOwnCharacter;
|
||||
@@ -18,16 +18,22 @@ const SHIP_NAME_RX = /u'|'/g;
|
||||
export const getShipName = (name: string) => {
|
||||
return name
|
||||
.replace(SHIP_NAME_RX, '')
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
})
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
});
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) =>
|
||||
String.fromCharCode(parseInt(grp, 16))
|
||||
)
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) =>
|
||||
String.fromCharCode(parseInt(grp, 16))
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => (
|
||||
<span className="mx-1 text-gray-400" aria-hidden="true">
|
||||
|
|
||||
</span>
|
||||
);
|
||||
|
||||
export const CharacterCard = ({
|
||||
compact,
|
||||
compact = false,
|
||||
isOwn,
|
||||
showSystem,
|
||||
showShipName,
|
||||
@@ -41,57 +47,114 @@ export const CharacterCard = ({
|
||||
});
|
||||
}, [char]);
|
||||
|
||||
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 && (
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)` }}
|
||||
const shipNameText = char.ship?.ship_name ? getShipName(char.ship.ship_name) : '';
|
||||
const tickerText = char.alliance_id ? char.alliance_ticker : char.corporation_ticker;
|
||||
const shipType = char.ship?.ship_type_info?.name;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
|
||||
<img
|
||||
src={`https://images.evetech.net/characters/${char.eve_id}/portrait`}
|
||||
alt={`${char.name} portrait`}
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
borderRadius: 0,
|
||||
flexShrink: 0,
|
||||
border: '1px solid #2b2b2b',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div
|
||||
className={clsx(classes.CharRow, 'w-full', {
|
||||
[classes.TwoColumns]: !char.ship,
|
||||
[classes.ThreeColumns]: char.ship,
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</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 className="flex flex-grow overflow-hidden text-left" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="text-gray-200">{char.name}</span>
|
||||
<Divider />
|
||||
{showShipName && shipNameText ? (
|
||||
<span className="text-indigo-300">{shipNameText}</span>
|
||||
) : (
|
||||
<span className="text-indigo-300">[{tickerText}]</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{shipType && (
|
||||
<div
|
||||
className="text-yellow-400 overflow-hidden text-ellipsis whitespace-nowrap flex-shrink-0"
|
||||
style={{ maxWidth: '120px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSystem && !compact && char.location?.solar_system_id && (
|
||||
<SystemView systemId={char.location.solar_system_id.toString()} useSystemsCache={useSystemsCache} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
const locationShown = showSystem && char.location?.solar_system_id;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs box-border')}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="w-full px-2 py-1 flex items-center gap-2" style={{ minWidth: 0 }}>
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{
|
||||
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
|
||||
minWidth: '33px',
|
||||
minHeight: '33px',
|
||||
width: '33px',
|
||||
height: '33px',
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-grow overflow-hidden" style={{ minWidth: 0 }}>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<span className="text-gray-200">{char.name}</span>
|
||||
<Divider />
|
||||
<span className="text-indigo-300">[{tickerText}]</span>
|
||||
</div>
|
||||
{locationShown ? (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<SystemView
|
||||
systemId={char?.location?.solar_system_id?.toString() || '' }
|
||||
useSystemsCache={useSystemsCache}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
shipNameText && (
|
||||
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{shipNameText}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{((shipType) || (locationShown && shipNameText)) && (
|
||||
<div className="flex-shrink-0 self-start">
|
||||
{shipType && (
|
||||
<div
|
||||
className="text-yellow-400 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
style={{ maxWidth: '200px' }}
|
||||
title={shipType}
|
||||
>
|
||||
{shipType}
|
||||
</div>
|
||||
)}
|
||||
{locationShown && shipNameText && (
|
||||
<div
|
||||
className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap text-right"
|
||||
style={{ maxWidth: '200px' }}
|
||||
>
|
||||
{shipNameText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import classes from './WdTooltip.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import classes from './WdTooltip.module.scss';
|
||||
|
||||
export enum TooltipPosition {
|
||||
default = 'default',
|
||||
@@ -24,16 +21,12 @@ 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;
|
||||
targetSelector?: string;
|
||||
}
|
||||
|
||||
export interface WdTooltipHandlers {
|
||||
show: MouseEventHandler;
|
||||
hide: MouseEventHandler;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export interface OffsetPosition {
|
||||
@@ -41,169 +34,263 @@ export interface OffsetPosition {
|
||||
left: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const WdTooltip = forwardRef((props: TooltipProps & WithClassName, ref: ForwardedRef<WdTooltipHandlers>) => {
|
||||
const { content, targetSelector, position: tPosition = TooltipPosition.default, className, offset = 5 } = props;
|
||||
export interface WdTooltipHandlers {
|
||||
show: (e?: React.MouseEvent) => void;
|
||||
hide: () => void;
|
||||
getIsMouseInside: () => boolean;
|
||||
}
|
||||
|
||||
interface TriggerInfo {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
rect: DOMRect;
|
||||
}
|
||||
|
||||
const LEAVE_DELAY = 100;
|
||||
|
||||
export const WdTooltip = forwardRef(function WdTooltip(
|
||||
{
|
||||
content,
|
||||
targetSelector,
|
||||
position: tPosition = TooltipPosition.default,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
ref: ForwardedRef<WdTooltipHandlers>
|
||||
) {
|
||||
// Always initialize position so we never have a null value.
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState<OffsetPosition | null>(null);
|
||||
const [ev, setEv] = useState<MouseEvent>();
|
||||
const [pos, setPos] = useState<OffsetPosition>({ left: 0, top: 0 });
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
|
||||
if (!tooltipRef.current) {
|
||||
return { left: newLeft, top: newTop };
|
||||
}
|
||||
const [triggerInfo, setTriggerInfo] = useState<TriggerInfo | null>(null);
|
||||
|
||||
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 10;
|
||||
}
|
||||
if (newLeft < 0) newLeft = 10;
|
||||
if (newTop < 0) newTop = 10;
|
||||
|
||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
||||
const rightEdge = newLeft + tooltipWidth + 10;
|
||||
if (rightEdge > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
|
||||
const bottomEdge = newTop + tooltipHeight + 10;
|
||||
if (bottomEdge > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
|
||||
return { left: newLeft, top: newTop };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !ev) {
|
||||
const scheduleHide = useCallback(() => {
|
||||
if (!interactive) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { clientX, clientY, target } = ev;
|
||||
|
||||
const targetBounds = (target as HTMLElement).getBoundingClientRect();
|
||||
const tooltipBounds = tooltipRef.current.getBoundingClientRect();
|
||||
|
||||
let offsetX = clientX;
|
||||
let offsetY = clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
offsetX = targetBounds.left - tooltipBounds.width - offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
if (offsetX <= 0) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
}
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
if (!hideTimeoutRef.current) {
|
||||
hideTimeoutRef.current = setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, LEAVE_DELAY);
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
offsetY = targetBounds.top - tooltipBounds.height - offset;
|
||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipBounds.width / 2;
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
|
||||
// default case
|
||||
setPosition(calcTooltipPosition({ x: clientX, y: clientY }));
|
||||
}, [calcTooltipPosition, ev, tPosition, offset]);
|
||||
}, [interactive]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: e => {
|
||||
setEv(e);
|
||||
show: (e?: React.MouseEvent) => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
if (e) {
|
||||
// Use e.currentTarget (or fallback to e.target) to determine the trigger element.
|
||||
const triggerEl = (e.currentTarget as HTMLElement) || (e.target as HTMLElement);
|
||||
if (triggerEl) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
setTriggerInfo({ clientX: e.clientX, clientY: e.clientY, rect });
|
||||
setPos(calcTooltipPosition({ x: e.clientX, y: e.clientY }));
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
setPosition(null);
|
||||
},
|
||||
hide: () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
setVisible(false);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (targetSelector == null) {
|
||||
// Recalculate position once the tooltip element has been rendered.
|
||||
useLayoutEffect(() => {
|
||||
if (!tooltipRef.current || !triggerInfo) return;
|
||||
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const { rect } = triggerInfo;
|
||||
let x = triggerInfo.clientX;
|
||||
let y = triggerInfo.clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
x = rect.left - tooltipBounds.width - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipBounds.height / 2;
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
return;
|
||||
}
|
||||
// Default case: use stored coordinates.
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}, [calcTooltipPosition, triggerInfo, tPosition, offset]);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const targetElement = e.target as HTMLElement;
|
||||
useEffect(() => {
|
||||
if (!targetSelector) return;
|
||||
|
||||
if (!targetElement) {
|
||||
setVisible(false);
|
||||
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;
|
||||
}
|
||||
|
||||
const nodesFound = [...(targetElement?.parentElement?.querySelectorAll(targetSelector) ?? [])];
|
||||
|
||||
if (!nodesFound.includes(targetElement)) {
|
||||
setVisible(false);
|
||||
return;
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
hideTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
if (tooltipRef.current) {
|
||||
const { clientX, clientY } = e;
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
let newLeft = clientX + 10;
|
||||
let newTop = clientY + 10;
|
||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
|
||||
let x = evt.clientX;
|
||||
let y = evt.clientY;
|
||||
|
||||
switch (tPosition) {
|
||||
case TooltipPosition.left: {
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.right: {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.top + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.top: {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
break;
|
||||
}
|
||||
case TooltipPosition.bottom: {
|
||||
x = rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
setPosition({ top: newTop, left: newLeft });
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}
|
||||
};
|
||||
|
||||
const deb = debounce(handleMouseMove, 10);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
document.addEventListener('mousemove', deb);
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
document.removeEventListener('mousemove', deb);
|
||||
const debounced = debounce(handleMouseMove, 15);
|
||||
const listener = (evt: Event) => {
|
||||
debounced(evt as MouseEvent);
|
||||
};
|
||||
}, [targetSelector]);
|
||||
|
||||
document.addEventListener('mousemove', listener);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', listener);
|
||||
debounced.cancel();
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition, scheduleHide]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideTimeoutRef.current) {
|
||||
clearTimeout(hideTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return createPortal(
|
||||
visible && (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
'pointer-events-none',
|
||||
'absolute px-2 py-2',
|
||||
'border rounded border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
{ ['invisible']: position === null },
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
top: position?.top ?? 0,
|
||||
left: position?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>
|
||||
),
|
||||
document.body,
|
||||
<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',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
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;
|
||||
}
|
||||
@@ -1,49 +1,57 @@
|
||||
import React, { HTMLProps, MouseEventHandler, useCallback, useRef } from 'react';
|
||||
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import { TooltipProps, WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { forwardRef, HTMLProps, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { WdTooltip, WdTooltipHandlers, TooltipProps } from '@/hooks/Mapper/components/ui-kit';
|
||||
import classes from './WdTooltipWrapper.module.scss';
|
||||
|
||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
export type WdTooltipWrapperProps = {
|
||||
content?: (() => React.ReactNode) | React.ReactNode;
|
||||
} & WithChildren &
|
||||
WithClassName &
|
||||
HTMLProps<HTMLDivElement> &
|
||||
content?: (() => ReactNode) | ReactNode;
|
||||
size?: TooltipSize;
|
||||
interactive?: boolean;
|
||||
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = ({
|
||||
className,
|
||||
children,
|
||||
content,
|
||||
offset,
|
||||
position,
|
||||
targetSelector,
|
||||
...props
|
||||
}: WdTooltipWrapperProps) => {
|
||||
const tooltipRef = useRef<WdTooltipHandlers>(null);
|
||||
const handleShowDeleteTooltip: MouseEventHandler = useCallback(e => tooltipRef.current?.show(e), []);
|
||||
const handleHideDeleteTooltip: MouseEventHandler = useCallback(e => tooltipRef.current?.hide(e), []);
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
(
|
||||
{ className, children, content, offset, position, targetSelector, interactive = false, size, ...props },
|
||||
forwardedRef,
|
||||
) => {
|
||||
const suffix = Math.random().toString(36).slice(2, 7);
|
||||
const autoClass = `wdTooltipAutoTrigger-${suffix}`;
|
||||
const finalTargetSelector = targetSelector || `.${autoClass}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(classes.WdTooltipWrapperRoot, className)}
|
||||
{...props}
|
||||
{...(content && {
|
||||
onMouseEnter: handleShowDeleteTooltip,
|
||||
onMouseLeave: handleHideDeleteTooltip,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
return (
|
||||
<div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}>
|
||||
{targetSelector ? <>{children}</> : <div className={autoClass}>{children}</div>}
|
||||
|
||||
<WdTooltip
|
||||
ref={forwardedRef}
|
||||
offset={offset}
|
||||
position={position}
|
||||
content={content}
|
||||
interactive={interactive}
|
||||
targetSelector={finalTargetSelector}
|
||||
className={size ? sizeClass(size) : undefined}
|
||||
/>
|
||||
</div>
|
||||
<WdTooltip
|
||||
ref={tooltipRef}
|
||||
offset={offset}
|
||||
position={position}
|
||||
content={content}
|
||||
targetSelector={targetSelector}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
WdTooltipWrapper.displayName = 'WdTooltipWrapper';
|
||||
|
||||
function sizeClass(size: TooltipSize) {
|
||||
switch (size) {
|
||||
case 'xs':
|
||||
return classes.wdTooltipSizeXs;
|
||||
case 'sm':
|
||||
return classes.wdTooltipSizeSm;
|
||||
case 'md':
|
||||
return classes.wdTooltipSizeMd;
|
||||
case 'lg':
|
||||
return classes.wdTooltipSizeLg;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +76,22 @@ export const WindowWrapper = ({ onResize, onDrag, ...window }: WindowWrapperProp
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export type ViewPortProps = { w: number; h: number };
|
||||
export type WindowsManagerOnChange = (props: { windows: WindowProps[]; viewPort: ViewPortProps }) => void;
|
||||
|
||||
type WindowManagerProps = {
|
||||
windows: WindowProps[];
|
||||
viewPort?: ViewPortProps;
|
||||
dragSelector?: string;
|
||||
onChange?(windows: WindowProps[]): void;
|
||||
onChange?: WindowsManagerOnChange;
|
||||
};
|
||||
|
||||
export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWindows, dragSelector, onChange }) => {
|
||||
export const WindowManager: React.FC<WindowManagerProps> = ({
|
||||
windows: initialWindows,
|
||||
viewPort,
|
||||
dragSelector,
|
||||
onChange,
|
||||
}) => {
|
||||
const [windows, setWindows] = useState(
|
||||
initialWindows.map((window, index) => ({
|
||||
...window,
|
||||
@@ -91,6 +99,16 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
})),
|
||||
);
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
refPrevSize.current = viewPort;
|
||||
}, [viewPort]);
|
||||
|
||||
useEffect(() => {
|
||||
setWindows(initialWindows.slice(0));
|
||||
}, [initialWindows]);
|
||||
@@ -102,14 +120,15 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
const startMousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const startWindowStateRef = useRef<{ x: number; y: number; width: number; height: number }>(DefaultWindowState);
|
||||
|
||||
const ref = useRef({ windows, onChange });
|
||||
ref.current = { windows, onChange };
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
const ref = useRef({ windows, viewPort, onChange });
|
||||
ref.current = { windows, viewPort, onChange };
|
||||
|
||||
const onDebouncedChange = useMemo(() => {
|
||||
return debounce(() => {
|
||||
ref.current.onChange?.(ref.current.windows);
|
||||
ref.current.onChange?.({
|
||||
windows: ref.current.windows,
|
||||
viewPort: refPrevSize.current,
|
||||
});
|
||||
}, 20);
|
||||
}, []);
|
||||
|
||||
@@ -336,7 +355,7 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
|
||||
// Handle resize of the container and reposition windows
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
if (ref.current.viewPort == null && containerRef.current) {
|
||||
refPrevSize.current = { w: containerRef.current.clientWidth, h: containerRef.current.clientHeight };
|
||||
}
|
||||
|
||||
@@ -384,10 +403,14 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
next.position.y = container.clientHeight - next.size.height - SNAP_GAP;
|
||||
}
|
||||
|
||||
if (next.position.y < 0) {
|
||||
if (next.position.y < SNAP_GAP) {
|
||||
next.position.y = 0;
|
||||
}
|
||||
|
||||
if (next.position.x < SNAP_GAP) {
|
||||
next.position.x = SNAP_GAP;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,3 +65,77 @@ export const REGIONS_MAP: Record<number, Spaces> = {
|
||||
[Regions.TashMurkon]: Spaces.Amarr,
|
||||
[Regions.VergeVendor]: Spaces.Gallente,
|
||||
};
|
||||
|
||||
export type K162Type = {
|
||||
label: string;
|
||||
value: string;
|
||||
whClassName: string;
|
||||
};
|
||||
|
||||
export const K162_TYPES: K162Type[] = [
|
||||
{
|
||||
label: 'Hi-Sec',
|
||||
value: 'hs',
|
||||
whClassName: 'A641',
|
||||
},
|
||||
{
|
||||
label: 'Low-Sec',
|
||||
value: 'ls',
|
||||
whClassName: 'J377',
|
||||
},
|
||||
{
|
||||
label: 'Null-Sec',
|
||||
value: 'ns',
|
||||
whClassName: 'C248',
|
||||
},
|
||||
{
|
||||
label: 'C1',
|
||||
value: 'c1',
|
||||
whClassName: 'E004',
|
||||
},
|
||||
{
|
||||
label: 'C2',
|
||||
value: 'c2',
|
||||
whClassName: 'D382',
|
||||
},
|
||||
{
|
||||
label: 'C3',
|
||||
value: 'c3',
|
||||
whClassName: 'L477',
|
||||
},
|
||||
{
|
||||
label: 'C4',
|
||||
value: 'c4',
|
||||
whClassName: 'M001',
|
||||
},
|
||||
{
|
||||
label: 'C5',
|
||||
value: 'c5',
|
||||
whClassName: 'L614',
|
||||
},
|
||||
{
|
||||
label: 'C6',
|
||||
value: 'c6',
|
||||
whClassName: 'G008',
|
||||
},
|
||||
{
|
||||
label: 'C13',
|
||||
value: 'c13',
|
||||
whClassName: 'A009',
|
||||
},
|
||||
{
|
||||
label: 'Thera',
|
||||
value: 'thera',
|
||||
whClassName: 'F353',
|
||||
},
|
||||
{
|
||||
label: 'Pochven',
|
||||
value: 'pochven',
|
||||
whClassName: 'F216',
|
||||
},
|
||||
];
|
||||
|
||||
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
|
||||
(acc, x) => ({ ...acc, [x.value]: x }),
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SignatureCustomInfo } from '@/hooks/Mapper/types';
|
||||
|
||||
export const parseSignatureCustomInfo = (str: string | undefined): SignatureCustomInfo => {
|
||||
if (str == null || str === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(str);
|
||||
};
|
||||
@@ -1,21 +1,27 @@
|
||||
import { ContextStoreDataUpdate, useContextStore } from '@/hooks/Mapper/utils';
|
||||
import { createContext, Dispatch, ForwardedRef, forwardRef, SetStateAction, useContext, useEffect } from 'react';
|
||||
import { MapUnionTypes, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
|
||||
import {
|
||||
CommandLinkSignatureToSystem,
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import {
|
||||
ToggleWidgetVisibility,
|
||||
UpdateWidgetSettingsFunc,
|
||||
useStoreWidgets,
|
||||
WindowStoreInfo,
|
||||
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
|
||||
import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types';
|
||||
import { WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { DetailedKill } from '../types/kills';
|
||||
|
||||
export type MapRootData = MapUnionTypes & {
|
||||
selectedSystems: string[];
|
||||
selectedConnections: Pick<SolarSystemConnection, 'source' | 'target'>[];
|
||||
linkSignatureToSystem: CommandLinkSignatureToSystem | null;
|
||||
detailedKills: Record<string, DetailedKill[]>;
|
||||
};
|
||||
|
||||
const INITIAL_DATA: MapRootData = {
|
||||
@@ -31,11 +37,12 @@ const INITIAL_DATA: MapRootData = {
|
||||
routes: undefined,
|
||||
kills: [],
|
||||
connections: [],
|
||||
|
||||
detailedKills: {},
|
||||
selectedSystems: [],
|
||||
selectedConnections: [],
|
||||
userPermissions: {},
|
||||
options: {},
|
||||
isSubscriptionActive: false,
|
||||
linkSignatureToSystem: null,
|
||||
};
|
||||
|
||||
@@ -80,7 +87,7 @@ export interface MapRootContextProps {
|
||||
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
|
||||
windowsSettings: WindowStoreInfo;
|
||||
toggleWidgetVisibility: ToggleWidgetVisibility;
|
||||
updateWidgetSettings: UpdateWidgetSettingsFunc;
|
||||
updateWidgetSettings: WindowsManagerOnChange;
|
||||
resetWidgets: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,18 @@ import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoa
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { DetailedKill } from '@/hooks/Mapper/types/kills';
|
||||
export const useCommandsSystems = () => {
|
||||
const {
|
||||
update,
|
||||
data: { systems, systemSignatures },
|
||||
data: { systems, systemSignatures, detailedKills },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const { addSystemStatic } = useLoadSystemStatic({ systems: [] });
|
||||
|
||||
const ref = useRef({ systems, systemSignatures, update, addSystemStatic });
|
||||
ref.current = { systems, systemSignatures, update, addSystemStatic };
|
||||
const ref = useRef({ systems, systemSignatures, update, addSystemStatic, detailedKills });
|
||||
ref.current = { systems, systemSignatures, update, addSystemStatic, detailedKills };
|
||||
|
||||
const addSystems = useCallback((systemsToAdd: CommandAddSystems) => {
|
||||
const { update, addSystemStatic, systems } = ref.current;
|
||||
@@ -84,5 +85,23 @@ export const useCommandsSystems = () => {
|
||||
update({ linkSignatureToSystem: command }, true);
|
||||
}, []);
|
||||
|
||||
return { addSystems, removeSystems, updateSystems, updateSystemSignatures, updateLinkSignatureToSystem };
|
||||
const updateDetailedKills = useCallback((newKillsMap: Record<string, DetailedKill[]>) => {
|
||||
const { update, detailedKills } = ref.current;
|
||||
|
||||
const updated = { ...detailedKills };
|
||||
for (const [systemId, killsArr] of Object.entries(newKillsMap)) {
|
||||
updated[systemId] = killsArr;
|
||||
}
|
||||
|
||||
update({ detailedKills: updated }, true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
addSystems,
|
||||
removeSystems,
|
||||
updateSystems,
|
||||
updateSystemSignatures,
|
||||
updateLinkSignatureToSystem,
|
||||
updateDetailedKills,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useMapInit = () => {
|
||||
hubs,
|
||||
user_permissions,
|
||||
options,
|
||||
is_subscription_active,
|
||||
}: CommandInit) => {
|
||||
const updateData: Partial<MapRootData> = {};
|
||||
|
||||
@@ -65,6 +66,8 @@ export const useMapInit = () => {
|
||||
updateData.options = options;
|
||||
}
|
||||
|
||||
updateData.isSubscriptionActive = is_subscription_active;
|
||||
|
||||
if (system_static_infos) {
|
||||
system_static_infos.forEach(static_info => {
|
||||
addSystemStatic(static_info);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CommandCharactersUpdated,
|
||||
CommandCharacterUpdated,
|
||||
CommandInit,
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
@@ -29,11 +30,18 @@ import {
|
||||
} from './api';
|
||||
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { DetailedKill } from '../../types/kills';
|
||||
|
||||
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapInit = useMapInit();
|
||||
const { addSystems, removeSystems, updateSystems, updateSystemSignatures, updateLinkSignatureToSystem } =
|
||||
useCommandsSystems();
|
||||
const {
|
||||
addSystems,
|
||||
removeSystems,
|
||||
updateSystems,
|
||||
updateSystemSignatures,
|
||||
updateLinkSignatureToSystem,
|
||||
updateDetailedKills,
|
||||
} = useCommandsSystems();
|
||||
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
|
||||
const { charactersUpdated, characterAdded, characterRemoved, characterUpdated, presentCharacters } =
|
||||
useCommandsCharacters();
|
||||
@@ -111,6 +119,10 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
} from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { SNAP_GAP } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { SNAP_GAP, WindowsManagerOnChange } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
|
||||
export type StoredWindowProps = Omit<WindowProps, 'content'>;
|
||||
export type WindowStoreInfo = {
|
||||
version: number;
|
||||
windows: StoredWindowProps[];
|
||||
visible: WidgetsIds[];
|
||||
viewPort?: { w: number; h: number } | undefined;
|
||||
};
|
||||
export type UpdateWidgetSettingsFunc = (widgets: WindowProps[]) => void;
|
||||
export type ToggleWidgetVisibility = (widgetId: WidgetsIds) => void;
|
||||
@@ -33,7 +34,7 @@ export const useStoreWidgets = () => {
|
||||
const ref = useRef({ windowsSettings, setWindowsSettings });
|
||||
ref.current = { windowsSettings, setWindowsSettings };
|
||||
|
||||
const updateWidgetSettings: UpdateWidgetSettingsFunc = useCallback(newWindows => {
|
||||
const updateWidgetSettings: WindowsManagerOnChange = useCallback(({ windows, viewPort }) => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
setWindowsSettings(({ version, visible /*, windows*/ }: WindowStoreInfo) => {
|
||||
@@ -41,13 +42,14 @@ export const useStoreWidgets = () => {
|
||||
version,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
windows: DEFAULT_WIDGETS.map(({ content, ...x }) => {
|
||||
const windowProp = newWindows.find(j => j.id === x.id);
|
||||
const windowProp = windows.find(j => j.id === x.id);
|
||||
if (windowProp) {
|
||||
return windowProp;
|
||||
}
|
||||
|
||||
return x;
|
||||
}),
|
||||
viewPort,
|
||||
visible,
|
||||
};
|
||||
});
|
||||
@@ -92,7 +94,7 @@ export const useStoreWidgets = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { version, windows, visible } = JSON.parse(raw) as WindowStoreInfo;
|
||||
const { version, windows, visible, viewPort } = JSON.parse(raw) as WindowStoreInfo;
|
||||
if (!version || CURRENT_WINDOWS_VERSION > version) {
|
||||
setWindowsSettings(getDefaultWidgetProps());
|
||||
}
|
||||
@@ -104,6 +106,7 @@ export const useStoreWidgets = () => {
|
||||
version: CURRENT_WINDOWS_VERSION,
|
||||
windows: out as WindowProps[],
|
||||
visible,
|
||||
viewPort,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -2,3 +2,38 @@ export type Kill = {
|
||||
solar_system_id: number;
|
||||
kills: number;
|
||||
};
|
||||
|
||||
export interface DetailedKill {
|
||||
killmail_id: number;
|
||||
solar_system_id: number;
|
||||
kill_time?: string;
|
||||
|
||||
zkb?: Record<string, unknown>;
|
||||
|
||||
victim_char_id?: number | null;
|
||||
victim_char_name?: string;
|
||||
victim_corp_id?: number | null;
|
||||
victim_corp_ticker?: string;
|
||||
victim_corp_name?: string;
|
||||
victim_alliance_id?: number | null;
|
||||
victim_alliance_ticker?: string;
|
||||
victim_alliance_name?: string;
|
||||
victim_ship_type_id?: number | null;
|
||||
victim_ship_name?: string;
|
||||
|
||||
|
||||
final_blow_char_id?: number | null;
|
||||
final_blow_char_name?: string;
|
||||
final_blow_corp_id?: number | null;
|
||||
final_blow_corp_ticker?: string;
|
||||
final_blow_corp_name?: string;
|
||||
final_blow_alliance_id?: number | null;
|
||||
final_blow_alliance_ticker?: string;
|
||||
final_blow_alliance_name?: string;
|
||||
final_blow_ship_type_id?: number | null;
|
||||
final_blow_ship_name?: string;
|
||||
|
||||
attacker_count?: number | null;
|
||||
total_value?: number | null;
|
||||
npc?: boolean;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes.ts';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
import { SignatureGroup, UserPermissions } from '@/hooks/Mapper/types';
|
||||
|
||||
export enum Commands {
|
||||
init = 'init',
|
||||
@@ -21,6 +21,7 @@ export enum Commands {
|
||||
updateConnection = 'update_connection',
|
||||
mapUpdated = 'map_updated',
|
||||
killsUpdated = 'kills_updated',
|
||||
detailedKillsUpdated = 'detailed_kills_updated',
|
||||
routes = 'routes',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
@@ -43,6 +44,7 @@ export type Command =
|
||||
| Commands.updateConnection
|
||||
| Commands.mapUpdated
|
||||
| Commands.killsUpdated
|
||||
| Commands.detailedKillsUpdated
|
||||
| Commands.routes
|
||||
| Commands.selectSystem
|
||||
| Commands.centerSystem
|
||||
@@ -76,9 +78,11 @@ export type CommandCharacterRemoved = CharacterTypeRaw;
|
||||
export type CommandCharacterUpdated = CharacterTypeRaw;
|
||||
export type CommandPresentCharacters = string[];
|
||||
export type CommandUpdateConnection = SolarSystemConnection;
|
||||
export type CommandSignaturesUpdated = string;
|
||||
export type CommandMapUpdated = Partial<CommandInit>;
|
||||
export type CommandRoutes = RoutesList;
|
||||
export type CommandKillsUpdated = Kill[];
|
||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
|
||||
export type CommandSelectSystem = string | undefined;
|
||||
export type CommandCenterSystem = string | undefined;
|
||||
export type CommandLinkSignatureToSystem = {
|
||||
@@ -103,6 +107,7 @@ export interface CommandData {
|
||||
[Commands.mapUpdated]: CommandMapUpdated;
|
||||
[Commands.routes]: CommandRoutes;
|
||||
[Commands.killsUpdated]: CommandKillsUpdated;
|
||||
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
|
||||
[Commands.selectSystem]: CommandSelectSystem;
|
||||
[Commands.centerSystem]: CommandCenterSystem;
|
||||
[Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
|
||||
@@ -151,6 +156,8 @@ export enum OutCommand {
|
||||
linkSignatureToSystem = 'link_signature_to_system',
|
||||
getCorporationNames = 'get_corporation_names',
|
||||
getCorporationTicker = 'get_corporation_ticker',
|
||||
getSystemKills = 'get_system_kills',
|
||||
getSystemsKills = 'get_systems_kills',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
@@ -161,4 +168,5 @@ export enum OutCommand {
|
||||
searchSystems = 'search_systems',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;
|
||||
|
||||
@@ -22,4 +22,5 @@ export type MapUnionTypes = {
|
||||
connections: SolarSystemConnection[];
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
options: Record<string, string | boolean>;
|
||||
isSubscriptionActive: boolean;
|
||||
};
|
||||
|
||||
@@ -26,15 +26,20 @@ export type GroupType = {
|
||||
h: number;
|
||||
};
|
||||
|
||||
export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
isEOL?: boolean;
|
||||
};
|
||||
|
||||
export type SystemSignature = {
|
||||
eve_id: string;
|
||||
kind: SignatureKind;
|
||||
name: string;
|
||||
// SignatureCustomInfo
|
||||
custom_info?: string;
|
||||
description?: string;
|
||||
group: SignatureGroup;
|
||||
type: string;
|
||||
k162Type?: string;
|
||||
linked_system?: SolarSystemStaticInfoRaw;
|
||||
inserted_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { XYPosition } from 'reactflow';
|
||||
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { DetailedKill } from './kills';
|
||||
|
||||
export enum SolarSystemStaticInfoRawNames {
|
||||
regionId = 'region_id',
|
||||
|
||||
@@ -25,10 +25,11 @@
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.6.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-event-hook": "^3.1.2",
|
||||
"react-flow-renderer": "^10.3.17",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"react-usestateref": "^1.0.9",
|
||||
"reactflow": "^11.11.4",
|
||||
@@ -42,12 +43,11 @@
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.2.1",
|
||||
"@types/react-grid-layout": "^1.3.4",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"child_process": "^1.0.2",
|
||||
@@ -82,5 +82,6 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
BIN
assets/static/images/news/01-27-zkill-widget/cover.png
Executable file
BIN
assets/static/images/news/01-27-zkill-widget/cover.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 409 KiB |
BIN
assets/static/images/news/01-27-zkill-widget/enable-zkill.png
Executable file
BIN
assets/static/images/news/01-27-zkill-widget/enable-zkill.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -1,24 +1,10 @@
|
||||
import cdn from 'vite-plugin-cdn-import';
|
||||
import path from 'path';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default {
|
||||
publicDir: './static',
|
||||
plugins: [
|
||||
cdn({
|
||||
modules: [
|
||||
{
|
||||
name: 'react',
|
||||
var: 'React',
|
||||
path: `umd/react.production.min.js`,
|
||||
},
|
||||
{
|
||||
name: 'react-dom',
|
||||
var: 'ReactDOM',
|
||||
path: `umd/react-dom.production.min.js`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
plugins: [react()],
|
||||
build: {
|
||||
target: 'es2018',
|
||||
format: 'esm',
|
||||
@@ -27,13 +13,8 @@ export default {
|
||||
emptyOutDir: true,
|
||||
assetsInlineLimit: 0,
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom'],
|
||||
input: ['app.tsx'],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
},
|
||||
entryFileNames: 'assets/[name].js',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name][extname]',
|
||||
|
||||
319
assets/yarn.lock
319
assets/yarn.lock
@@ -28,12 +28,26 @@
|
||||
"@babel/highlight" "^7.24.2"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.2":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
|
||||
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@babel/compat-data@^7.23.5":
|
||||
version "7.24.4"
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz"
|
||||
integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==
|
||||
|
||||
"@babel/core@^7.14.8", "@babel/core@^7.24.5":
|
||||
"@babel/compat-data@^7.26.5":
|
||||
version "7.26.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.5.tgz#df93ac37f4417854130e21d72c66ff3d4b897fc7"
|
||||
integrity sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==
|
||||
|
||||
"@babel/core@^7.14.8":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz"
|
||||
integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==
|
||||
@@ -54,6 +68,27 @@
|
||||
json5 "^2.2.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/core@^7.26.0":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.7.tgz#0439347a183b97534d52811144d763a17f9d2b24"
|
||||
integrity sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.2.0"
|
||||
"@babel/code-frame" "^7.26.2"
|
||||
"@babel/generator" "^7.26.5"
|
||||
"@babel/helper-compilation-targets" "^7.26.5"
|
||||
"@babel/helper-module-transforms" "^7.26.0"
|
||||
"@babel/helpers" "^7.26.7"
|
||||
"@babel/parser" "^7.26.7"
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/traverse" "^7.26.7"
|
||||
"@babel/types" "^7.26.7"
|
||||
convert-source-map "^2.0.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
json5 "^2.2.3"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/generator@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz"
|
||||
@@ -64,6 +99,17 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
"@babel/generator@^7.26.5":
|
||||
version "7.26.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.5.tgz#e44d4ab3176bbcaf78a5725da5f1dc28802a9458"
|
||||
integrity sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.26.5"
|
||||
"@babel/types" "^7.26.5"
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.23.6":
|
||||
version "7.23.6"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz"
|
||||
@@ -75,6 +121,17 @@
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.26.5":
|
||||
version "7.26.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8"
|
||||
integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.26.5"
|
||||
"@babel/helper-validator-option" "^7.25.9"
|
||||
browserslist "^4.24.0"
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-environment-visitor@^7.22.20":
|
||||
version "7.22.20"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz"
|
||||
@@ -102,6 +159,14 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.24.0"
|
||||
|
||||
"@babel/helper-module-imports@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715"
|
||||
integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==
|
||||
dependencies:
|
||||
"@babel/traverse" "^7.25.9"
|
||||
"@babel/types" "^7.25.9"
|
||||
|
||||
"@babel/helper-module-transforms@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz"
|
||||
@@ -113,11 +178,25 @@
|
||||
"@babel/helper-split-export-declaration" "^7.24.5"
|
||||
"@babel/helper-validator-identifier" "^7.24.5"
|
||||
|
||||
"@babel/helper-module-transforms@^7.26.0":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae"
|
||||
integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
"@babel/traverse" "^7.25.9"
|
||||
|
||||
"@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz"
|
||||
integrity sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==
|
||||
|
||||
"@babel/helper-plugin-utils@^7.25.9":
|
||||
version "7.26.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35"
|
||||
integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==
|
||||
|
||||
"@babel/helper-simple-access@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz"
|
||||
@@ -137,16 +216,31 @@
|
||||
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz"
|
||||
integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==
|
||||
|
||||
"@babel/helper-string-parser@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
|
||||
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz"
|
||||
integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
|
||||
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
|
||||
|
||||
"@babel/helper-validator-option@^7.23.5":
|
||||
version "7.23.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz"
|
||||
integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==
|
||||
|
||||
"@babel/helper-validator-option@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72"
|
||||
integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==
|
||||
|
||||
"@babel/helpers@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz"
|
||||
@@ -156,6 +250,14 @@
|
||||
"@babel/traverse" "^7.24.5"
|
||||
"@babel/types" "^7.24.5"
|
||||
|
||||
"@babel/helpers@^7.26.7":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.7.tgz#fd1d2a7c431b6e39290277aacfd8367857c576a4"
|
||||
integrity sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==
|
||||
dependencies:
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/types" "^7.26.7"
|
||||
|
||||
"@babel/highlight@^7.24.2":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz"
|
||||
@@ -171,20 +273,41 @@
|
||||
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz"
|
||||
integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self@^7.14.5", "@babel/plugin-transform-react-jsx-self@^7.24.5":
|
||||
"@babel/parser@^7.25.9", "@babel/parser@^7.26.5", "@babel/parser@^7.26.7":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.7.tgz#e114cd099e5f7d17b05368678da0fb9f69b3385c"
|
||||
integrity sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==
|
||||
dependencies:
|
||||
"@babel/types" "^7.26.7"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self@^7.14.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz"
|
||||
integrity sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.24.5"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source@^7.14.5", "@babel/plugin-transform-react-jsx-source@^7.24.1":
|
||||
"@babel/plugin-transform-react-jsx-self@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz#c0b6cae9c1b73967f7f9eb2fca9536ba2fad2858"
|
||||
integrity sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.25.9"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source@^7.14.5":
|
||||
version "7.24.1"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz"
|
||||
integrity sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz#4c6b8daa520b5f155b5fb55547d7c9fa91417503"
|
||||
integrity sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.25.9"
|
||||
|
||||
"@babel/runtime@^7.12.5":
|
||||
version "7.25.0"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz"
|
||||
@@ -215,6 +338,15 @@
|
||||
"@babel/parser" "^7.24.0"
|
||||
"@babel/types" "^7.24.0"
|
||||
|
||||
"@babel/template@^7.25.9":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016"
|
||||
integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.25.9"
|
||||
"@babel/parser" "^7.25.9"
|
||||
"@babel/types" "^7.25.9"
|
||||
|
||||
"@babel/traverse@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz"
|
||||
@@ -231,6 +363,19 @@
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.7":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.7.tgz#99a0a136f6a75e7fb8b0a1ace421e0b25994b8bb"
|
||||
integrity sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.26.2"
|
||||
"@babel/generator" "^7.26.5"
|
||||
"@babel/parser" "^7.26.7"
|
||||
"@babel/template" "^7.25.9"
|
||||
"@babel/types" "^7.26.7"
|
||||
debug "^4.3.1"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.24.5":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz"
|
||||
@@ -240,6 +385,14 @@
|
||||
"@babel/helper-validator-identifier" "^7.24.5"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.25.9", "@babel/types@^7.26.5", "@babel/types@^7.26.7":
|
||||
version "7.26.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.7.tgz#5e2b89c0768e874d4d061961f3a5a153d71dc17a"
|
||||
integrity sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.25.9"
|
||||
"@babel/helper-validator-identifier" "^7.25.9"
|
||||
|
||||
"@esbuild/aix-ppc64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
||||
@@ -951,19 +1104,10 @@
|
||||
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz"
|
||||
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||
|
||||
"@types/react-dom@18.2.1":
|
||||
version "18.2.1"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.1.tgz"
|
||||
integrity sha512-8QZEV9+Kwy7tXFmjJrp3XUKQSs9LTnE0KnoUb0YCguWBiNW0Yfb2iBMYZ08WPg35IR6P3Z0s00B15SwZnO26+w==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-grid-layout@^1.3.4":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz"
|
||||
integrity sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@types/react-dom@^18.3.1":
|
||||
version "18.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716"
|
||||
integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==
|
||||
|
||||
"@types/react-transition-group@^4.4.1":
|
||||
version "4.4.10"
|
||||
@@ -972,7 +1116,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@18.2.0":
|
||||
"@types/react@*":
|
||||
version "18.2.0"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz"
|
||||
integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==
|
||||
@@ -981,6 +1125,14 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^18.3.12":
|
||||
version "18.3.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b"
|
||||
integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/resize-observer-browser@^0.1.7":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.11.tgz"
|
||||
@@ -1098,14 +1250,14 @@
|
||||
"@rollup/pluginutils" "^4.1.1"
|
||||
react-refresh "^0.10.0"
|
||||
|
||||
"@vitejs/plugin-react@^4.3.0":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz"
|
||||
integrity sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==
|
||||
"@vitejs/plugin-react@^4.3.3":
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz#c64be10b54c4640135a5b28a2432330e88ad7c20"
|
||||
integrity sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==
|
||||
dependencies:
|
||||
"@babel/core" "^7.24.5"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.24.5"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.24.1"
|
||||
"@babel/core" "^7.26.0"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.25.9"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.25.9"
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.14.2"
|
||||
|
||||
@@ -1324,6 +1476,16 @@ browserslist@^4.22.2, browserslist@^4.23.0:
|
||||
node-releases "^2.0.14"
|
||||
update-browserslist-db "^1.0.13"
|
||||
|
||||
browserslist@^4.24.0:
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
|
||||
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001688"
|
||||
electron-to-chromium "^1.5.73"
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.1"
|
||||
|
||||
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
|
||||
@@ -1346,9 +1508,14 @@ 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==
|
||||
|
||||
caniuse-lite@^1.0.30001688:
|
||||
version "1.0.30001697"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz#040bbbb54463c4b4b3377c716b34a322d16e6fc7"
|
||||
integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==
|
||||
|
||||
chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
@@ -1401,12 +1568,7 @@ cliui@^8.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
clsx@^2.0.0, clsx@^2.1.1:
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
@@ -1660,6 +1822,11 @@ electron-to-chromium@^1.4.668:
|
||||
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz"
|
||||
integrity sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==
|
||||
|
||||
electron-to-chromium@^1.5.73:
|
||||
version "1.5.91"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.91.tgz#cf5567f6853062493242133aefd4dc8dc8440abd"
|
||||
integrity sha512-sNSHHyq048PFmZY4S90ax61q+gLCs0X0YmcOII9wG9S2XwbVr+h4VW2wWhnbp/Eys3cCwTxVF292W3qPaxIapQ==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
|
||||
@@ -1820,6 +1987,11 @@ escalade@^3.1.1:
|
||||
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz"
|
||||
integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
|
||||
|
||||
escalade@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
||||
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
|
||||
@@ -1989,11 +2161,6 @@ fast-diff@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz"
|
||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
||||
|
||||
fast-equals@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz"
|
||||
integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==
|
||||
|
||||
fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
|
||||
@@ -2597,6 +2764,11 @@ jsesc@^2.5.1:
|
||||
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
|
||||
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
|
||||
|
||||
jsesc@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
|
||||
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
|
||||
|
||||
json-buffer@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz"
|
||||
@@ -2814,6 +2986,11 @@ node-releases@^2.0.14:
|
||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz"
|
||||
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
|
||||
|
||||
node-releases@^2.0.19:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
|
||||
@@ -2975,6 +3152,11 @@ picocolors@^1, picocolors@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
@@ -3139,7 +3321,7 @@ primereact@^10.6.5:
|
||||
"@types/react-transition-group" "^4.4.1"
|
||||
react-transition-group "^4.4.1"
|
||||
|
||||
prop-types@15.x, prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -3166,13 +3348,13 @@ react-dom@18.2.0:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-draggable@^4.0.3, react-draggable@^4.4.5:
|
||||
version "4.4.6"
|
||||
resolved "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz"
|
||||
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
|
||||
react-dom@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
|
||||
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
||||
dependencies:
|
||||
clsx "^1.1.1"
|
||||
prop-types "^15.8.1"
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-error-boundary@^4.0.13:
|
||||
version "4.0.13"
|
||||
@@ -3200,18 +3382,6 @@ react-flow-renderer@^10.3.17:
|
||||
d3-zoom "^3.0.0"
|
||||
zustand "^3.7.2"
|
||||
|
||||
react-grid-layout@^1.3.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.4.4.tgz"
|
||||
integrity sha512-7+Lg8E8O8HfOH5FrY80GCIR1SHTn2QnAYKh27/5spoz+OHhMmEhU/14gIkRzJOtympDPaXcVRX/nT1FjmeOUmQ==
|
||||
dependencies:
|
||||
clsx "^2.0.0"
|
||||
fast-equals "^4.0.3"
|
||||
prop-types "^15.8.1"
|
||||
react-draggable "^4.4.5"
|
||||
react-resizable "^3.0.5"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
react-hook-form@^7.53.1:
|
||||
version "7.53.1"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.1.tgz#3f2cd1ed2b3af99416a4ac674da2d526625add67"
|
||||
@@ -3232,14 +3402,6 @@ react-refresh@^0.14.2:
|
||||
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz"
|
||||
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
||||
|
||||
react-resizable@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz"
|
||||
integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
|
||||
dependencies:
|
||||
prop-types "15.x"
|
||||
react-draggable "^4.0.3"
|
||||
|
||||
react-transition-group@^4.4.1:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"
|
||||
@@ -3262,6 +3424,13 @@ react@18.2.0:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
react@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
reactflow@^11.11.4:
|
||||
version "11.11.4"
|
||||
resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653"
|
||||
@@ -3321,11 +3490,6 @@ require-directory@^2.1.1:
|
||||
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||
@@ -3445,6 +3609,13 @@ scheduler@^0.23.0:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
scheduler@^0.23.2:
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
semver@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
|
||||
@@ -3801,6 +3972,14 @@ update-browserslist-db@^1.0.13:
|
||||
escalade "^3.1.1"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
update-browserslist-db@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz#97e9c96ab0ae7bcac08e9ae5151d26e6bc6b5580"
|
||||
integrity sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==
|
||||
dependencies:
|
||||
escalade "^3.2.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
|
||||
|
||||
@@ -53,6 +53,11 @@ public_api_disabled =
|
||||
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
zkill_preload_disabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_ZKILL_PRELOAD_DISABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
map_subscriptions_enabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
|
||||
@@ -113,6 +118,7 @@ config :wanderer_app,
|
||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||
public_api_disabled: public_api_disabled,
|
||||
zkill_preload_disabled: zkill_preload_disabled,
|
||||
map_subscriptions_enabled: map_subscriptions_enabled,
|
||||
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
||||
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
|
||||
|
||||
@@ -127,7 +127,6 @@ defmodule WandererApp.Api.Map do
|
||||
update :update_api_key do
|
||||
accept [:public_api_key]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -13,39 +13,48 @@ defmodule WandererApp.Application do
|
||||
WandererAppWeb.Telemetry,
|
||||
WandererApp.Vault,
|
||||
WandererApp.Repo,
|
||||
|
||||
{Phoenix.PubSub, name: WandererApp.PubSub, adapter_name: Phoenix.PubSub.PG2},
|
||||
{Finch, name: WandererApp.Finch},
|
||||
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch,
|
||||
pools: %{
|
||||
default: [
|
||||
size: 25, # number of connections per pool
|
||||
count: 2, # number of pools (so total 50 connections)
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
WandererApp.Cache,
|
||||
Supervisor.child_spec({Cachex, name: :system_static_info_cache},
|
||||
id: :system_static_info_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache},
|
||||
id: :ship_types_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache},
|
||||
id: :character_state_cache_worker
|
||||
),
|
||||
|
||||
Supervisor.child_spec({Cachex, name: :system_static_info_cache}, id: :system_static_info_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache}, id: :character_state_cache_worker),
|
||||
|
||||
WandererApp.Scheduler,
|
||||
|
||||
{Registry, keys: :unique, name: WandererApp.MapRegistry},
|
||||
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor, child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
|
||||
WandererApp.Zkb.Supervisor,
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
WandererApp.Character.TrackerManager,
|
||||
WandererApp.Map.Manager,
|
||||
WandererApp.Map.ZkbDataFetcher,
|
||||
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
] ++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
]
|
||||
++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: WandererApp.Supervisor]
|
||||
|
||||
Supervisor.start_link(children, opts)
|
||||
@@ -59,8 +68,6 @@ defmodule WandererApp.Application do
|
||||
end
|
||||
end
|
||||
|
||||
# Tell Phoenix to update the endpoint configuration
|
||||
# whenever the application is updated.
|
||||
@impl true
|
||||
def config_change(changed, _new, removed) do
|
||||
WandererAppWeb.Endpoint.config_change(changed, removed)
|
||||
@@ -72,5 +79,6 @@ defmodule WandererApp.Application do
|
||||
WandererApp.StartCorpWalletTrackerTask
|
||||
]
|
||||
|
||||
defp maybe_start_corp_wallet_tracker(_), do: []
|
||||
defp maybe_start_corp_wallet_tracker(_),
|
||||
do: []
|
||||
end
|
||||
|
||||
@@ -71,7 +71,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, info} ->
|
||||
{:ok, _info} ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ defmodule WandererApp.Env do
|
||||
def invites, do: get_key(:invites, false)
|
||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
||||
def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false)
|
||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins, do: get_key(:admins, [])
|
||||
def admin_username, do: get_key(:admin_username)
|
||||
@@ -39,4 +40,12 @@ defmodule WandererApp.Env do
|
||||
do: get_key(:map_connection_eol_expire_timeout_mins)
|
||||
|
||||
def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
|
||||
|
||||
@doc """
|
||||
A single map containing environment variables
|
||||
made available to react
|
||||
"""
|
||||
def to_client_env do
|
||||
%{detailedKillsDisabled: zkill_preload_disabled?()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,6 +24,9 @@ defmodule WandererApp.Esi do
|
||||
defdelegate find_routes(map_id, origin, hubs, routes_settings), to: WandererApp.Esi.ApiClient
|
||||
defdelegate search(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
|
||||
defdelegate get_killmail(killmail_id, killmail_hash, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
|
||||
|
||||
defdelegate set_autopilot_waypoint(
|
||||
add_to_beginning,
|
||||
clear_other_waypoints,
|
||||
|
||||
@@ -289,6 +289,16 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "killmail-#{killmail_id}-#{killmail_hash}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", _with_cache_opts(opts))
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "info-#{eve_id}",
|
||||
@@ -343,20 +353,29 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def get_character_ship(character_eve_id, opts \\ []),
|
||||
do: _get_character_auth_data(character_eve_id, "ship", opts)
|
||||
|
||||
def search(character_eve_id, opts \\ []),
|
||||
do: _search(character_eve_id, opts[:params][:search], opts)
|
||||
def search(character_eve_id, opts \\ []) do
|
||||
search_val = to_string(opts[:params][:search] || "")
|
||||
categories_val = to_string(opts[:params][:categories] || "character,alliance,corporation")
|
||||
|
||||
query_params = [
|
||||
{"search", search_val},
|
||||
{"categories", categories_val},
|
||||
{"language", "en-us"},
|
||||
{"strict", "false"},
|
||||
{"datasource", "tranquility"}
|
||||
]
|
||||
|
||||
merged_opts = Keyword.put(opts, :params, query_params)
|
||||
_search(character_eve_id, search_val, categories_val, merged_opts)
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "search-#{character_eve_id}-#{search |> Slug.slugify()}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp _search(character_eve_id, search, opts \\ []) when is_binary(search) do
|
||||
_get_character_auth_data(
|
||||
character_eve_id,
|
||||
"search",
|
||||
opts
|
||||
)
|
||||
cache: Cache,
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
_get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
defp _remove_intersection(pairs_arr) do
|
||||
|
||||
@@ -70,9 +70,14 @@ defmodule WandererApp.Map do
|
||||
def get_characters_limit(map_id),
|
||||
do: {:ok, map_id |> get_map!() |> Map.get(:characters_limit, 50)}
|
||||
|
||||
def is_subscription_active?(map_id) do
|
||||
def is_subscription_active?(map_id),
|
||||
do: is_subscription_active?(map_id, WandererApp.Env.map_subscriptions_enabled?())
|
||||
|
||||
def is_subscription_active?(_map_id, false), do: {:ok, true}
|
||||
|
||||
def is_subscription_active?(map_id, _map_subscriptions_enabled) do
|
||||
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
||||
not WandererApp.Env.map_subscriptions_enabled?() || plan != :alpha
|
||||
{:ok, plan != :alpha}
|
||||
end
|
||||
|
||||
def get_options(map_id),
|
||||
|
||||
@@ -6,114 +6,202 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||
|
||||
@interval :timer.seconds(15)
|
||||
@store_map_kills_timeout :timer.hours(1)
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
|
||||
# This means 120 “ticks” of 15s each → ~30 minutes
|
||||
@preload_cycle_ticks 120
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start(__MODULE__, [], name: __MODULE__)
|
||||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init([]) do
|
||||
{:ok, timer} = :timer.send_interval(@interval, :fetch_data)
|
||||
|
||||
{:ok, %{timer: timer}}
|
||||
def init(_arg) do
|
||||
{:ok, _timer_ref} = :timer.send_interval(@interval, :fetch_data)
|
||||
{:ok, %{iteration: 0}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:fetch_data, state) do
|
||||
def handle_info(:fetch_data, %{iteration: iteration} = state) do
|
||||
zkill_preload_disabled = WandererApp.Env.zkill_preload_disabled?()
|
||||
|
||||
WandererApp.Map.RegistryHelper.list_all_maps()
|
||||
|> Task.async_stream(
|
||||
fn %{id: map_id, pid: _server_pid} ->
|
||||
try do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.map_pid()
|
||||
|> case do
|
||||
pid when is_pid(pid) ->
|
||||
_update_map_kills(map_id)
|
||||
if WandererApp.Map.Server.map_pid(map_id) do
|
||||
update_map_kills(map_id)
|
||||
|
||||
nil ->
|
||||
:ok
|
||||
{:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?()
|
||||
|
||||
can_preload_zkill = not zkill_preload_disabled && is_subscription_active
|
||||
|
||||
if can_preload_zkill do
|
||||
update_detailed_map_kills(map_id)
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@logger.error(Exception.message(e))
|
||||
:ok
|
||||
end
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.map(fn _ -> :ok end)
|
||||
|> Enum.each(fn _ -> :ok end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
new_iteration = iteration + 1
|
||||
|
||||
@impl true
|
||||
def handle_info({ref, result}, state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
cond do
|
||||
zkill_preload_disabled ->
|
||||
# If preload is disabled, just update iteration
|
||||
{:noreply, %{state | iteration: new_iteration}}
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
new_iteration >= @preload_cycle_ticks ->
|
||||
Logger.info("[ZkbDataFetcher] Triggering a fresh kill preload pass ...")
|
||||
WandererApp.Zkb.KillsPreloader.run_preload_now()
|
||||
{:noreply, %{state | iteration: 0}}
|
||||
|
||||
defp _update_map_kills(map_id) do
|
||||
case WandererApp.Cache.lookup!("map_#{map_id}:started", false) do
|
||||
true ->
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:systems, Map.new())
|
||||
|> Enum.reduce(Map.new(), fn {solar_system_id, _system}, acc ->
|
||||
kills_count = WandererApp.Cache.get("zkb_kills_#{solar_system_id}")
|
||||
acc |> Map.put(solar_system_id, kills_count || 0)
|
||||
end)
|
||||
|> _maybe_broadcast_map_kills(map_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
{:noreply, %{state | iteration: new_iteration}}
|
||||
end
|
||||
end
|
||||
|
||||
defp _maybe_broadcast_map_kills(new_kills_map, map_id) do
|
||||
{:ok, old_kills_map} = WandererApp.Cache.lookup("map_#{map_id}:zkb_kills", Map.new())
|
||||
# Catch any async task results we aren't explicitly pattern-matching
|
||||
@impl true
|
||||
def handle_info({ref, _result}, state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
updated_kills_system_ids =
|
||||
new_kills_map
|
||||
|> Map.filter(fn {solar_system_id, new_kills_count} ->
|
||||
old_kills_count = old_kills_map |> Map.get(solar_system_id, 0)
|
||||
|
||||
new_kills_count != old_kills_count and
|
||||
new_kills_count > 0
|
||||
defp update_map_kills(map_id) do
|
||||
with_started_map(map_id, "basic kills update", fn ->
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:systems, %{})
|
||||
|> Enum.into(%{}, fn {solar_system_id, _system} ->
|
||||
kills_count = WandererApp.Cache.get("zkb_kills_#{solar_system_id}") || 0
|
||||
{solar_system_id, kills_count}
|
||||
end)
|
||||
|> Map.keys()
|
||||
|> maybe_broadcast_map_kills(map_id)
|
||||
end)
|
||||
end
|
||||
|
||||
removed_kills_system_ids =
|
||||
old_kills_map
|
||||
|> Map.filter(fn {solar_system_id, old_kills_count} ->
|
||||
new_kills_count = new_kills_map |> Map.get(solar_system_id, 0)
|
||||
defp update_detailed_map_kills(map_id) do
|
||||
with_started_map(map_id, "detailed kills update", fn ->
|
||||
systems =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:systems, %{})
|
||||
|
||||
old_kills_count > 0 and new_kills_count == 0
|
||||
end)
|
||||
|> Map.keys()
|
||||
# Old cache data
|
||||
old_ids_map = WandererApp.Cache.get("map_#{map_id}:zkb_ids") || %{}
|
||||
old_details_map = WandererApp.Cache.get("map_#{map_id}:zkb_detailed_kills") || %{}
|
||||
|
||||
(updated_kills_system_ids ++ removed_kills_system_ids)
|
||||
|> case do
|
||||
[] ->
|
||||
new_ids_map =
|
||||
Enum.into(systems, %{}, fn {solar_system_id, _} ->
|
||||
ids = KillsCache.get_system_killmail_ids(solar_system_id) |> MapSet.new()
|
||||
{solar_system_id, ids}
|
||||
end)
|
||||
|
||||
changed_systems =
|
||||
new_ids_map
|
||||
|> Enum.filter(fn {system_id, new_ids_set} ->
|
||||
old_set = MapSet.new(Map.get(old_ids_map, system_id, []))
|
||||
not MapSet.equal?(new_ids_set, old_set)
|
||||
end)
|
||||
|> Enum.map(&elem(&1, 0))
|
||||
|
||||
if changed_systems == [] do
|
||||
Logger.debug(fn -> "[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}" end)
|
||||
:ok
|
||||
else
|
||||
# Build new details for each changed system
|
||||
updated_details_map =
|
||||
Enum.reduce(changed_systems, old_details_map, fn system_id, acc ->
|
||||
kill_ids =
|
||||
new_ids_map
|
||||
|> Map.fetch!(system_id)
|
||||
|> MapSet.to_list()
|
||||
|
||||
system_ids ->
|
||||
:ok =
|
||||
WandererApp.Cache.put("map_#{map_id}:zkb_kills", new_kills_map,
|
||||
ttl: @store_map_kills_timeout
|
||||
)
|
||||
kill_details =
|
||||
kill_ids
|
||||
|> Enum.map(&KillsCache.get_killmail/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
Map.put(acc, system_id, kill_details)
|
||||
end)
|
||||
|
||||
updated_ids_map =
|
||||
Enum.reduce(changed_systems, old_ids_map, fn system_id, acc ->
|
||||
new_ids_list = new_ids_map[system_id] |> MapSet.to_list()
|
||||
Map.put(acc, system_id, new_ids_list)
|
||||
end)
|
||||
|
||||
WandererApp.Cache.put("map_#{map_id}:zkb_ids", updated_ids_map,
|
||||
ttl: :timer.hours(KillsCache.killmail_ttl())
|
||||
)
|
||||
|
||||
WandererApp.Cache.put("map_#{map_id}:zkb_detailed_kills", updated_details_map,
|
||||
ttl: :timer.hours(KillsCache.killmail_ttl())
|
||||
)
|
||||
|
||||
changed_data = Map.take(updated_details_map, changed_systems)
|
||||
|
||||
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :kills_updated,
|
||||
payload: new_kills_map |> Map.take(system_ids)
|
||||
event: :detailed_kills_updated,
|
||||
payload: changed_data
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_broadcast_map_kills(new_kills_map, map_id) do
|
||||
{:ok, old_kills_map} = WandererApp.Cache.lookup("map_#{map_id}:zkb_kills", %{})
|
||||
|
||||
# Use the union of keys from both the new and old maps
|
||||
all_system_ids = Map.keys(Map.merge(new_kills_map, old_kills_map))
|
||||
|
||||
changed_system_ids =
|
||||
Enum.filter(all_system_ids, fn system_id ->
|
||||
new_kills_count = Map.get(new_kills_map, system_id, 0)
|
||||
old_kills_count = Map.get(old_kills_map, system_id, 0)
|
||||
new_kills_count != old_kills_count and
|
||||
(new_kills_count > 0 or (old_kills_count > 0 and new_kills_count == 0))
|
||||
end)
|
||||
|
||||
if changed_system_ids == [] do
|
||||
:ok
|
||||
else
|
||||
:ok =
|
||||
WandererApp.Cache.put("map_#{map_id}:zkb_kills", new_kills_map,
|
||||
ttl: @store_map_kills_timeout
|
||||
)
|
||||
|
||||
payload = Map.take(new_kills_map, changed_system_ids)
|
||||
|
||||
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
|
||||
event: :kills_updated,
|
||||
payload: payload
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp with_started_map(map_id, label \\ "operation", fun) when is_function(fun, 0) do
|
||||
if WandererApp.Cache.lookup!("map_#{map_id}:started", false) do
|
||||
fun.()
|
||||
else
|
||||
Logger.debug(fn -> "[ZkbDataFetcher] Map #{map_id} not started => skipping #{label}" end)
|
||||
:ok
|
||||
end
|
||||
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} ->
|
||||
|
||||
311
lib/wanderer_app/zkb/zkb_kills_preloader.ex
Normal file
311
lib/wanderer_app/zkb/zkb_kills_preloader.ex
Normal file
@@ -0,0 +1,311 @@
|
||||
defmodule WandererApp.Zkb.KillsPreloader do
|
||||
@moduledoc """
|
||||
On startup, kicks off two passes (quick and expanded) to preload kills data.
|
||||
|
||||
There is also a `run_preload_now/0` function for manual triggering of the same logic.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider
|
||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||
|
||||
# ----------------
|
||||
# Configuration
|
||||
# ----------------
|
||||
|
||||
# (1) Quick pass
|
||||
@quick_limit 1
|
||||
@quick_hours 1
|
||||
|
||||
# (2) Expanded pass
|
||||
@expanded_limit 25
|
||||
@expanded_hours 24
|
||||
|
||||
# How many minutes back we look for “last active” maps
|
||||
@last_active_cutoff 30
|
||||
|
||||
# Default concurrency if not provided
|
||||
@default_max_concurrency 2
|
||||
|
||||
@doc """
|
||||
Starts the GenServer with optional opts (like `max_concurrency`).
|
||||
"""
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Public helper to explicitly request a fresh preload pass (both quick & expanded).
|
||||
"""
|
||||
def run_preload_now() do
|
||||
send(__MODULE__, :start_preload)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
state = %{
|
||||
phase: :idle,
|
||||
calls_count: 0,
|
||||
max_concurrency: Keyword.get(opts, :max_concurrency, @default_max_concurrency)
|
||||
}
|
||||
|
||||
# Kick off the preload passes once at startup
|
||||
send(self(), :start_preload)
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:start_preload, state) do
|
||||
# Gather last-active maps (or fallback).
|
||||
cutoff_time =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-@last_active_cutoff, :minute)
|
||||
|
||||
last_active_maps_result = WandererApp.Api.MapState.get_last_active(cutoff_time)
|
||||
last_active_maps = resolve_last_active_maps(last_active_maps_result)
|
||||
active_maps_with_subscription = get_active_maps_with_subscription(last_active_maps)
|
||||
|
||||
# Gather systems from those maps
|
||||
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(last_active_maps)} map(s)
|
||||
" end)
|
||||
|
||||
# ---- QUICK PASS ----
|
||||
state_quick = %{state | phase: :quick_pass}
|
||||
|
||||
{time_quick_ms, state_after_quick} =
|
||||
measure_execution_time(fn ->
|
||||
do_pass(unique_systems, :quick, @quick_hours, @quick_limit, state_quick)
|
||||
end)
|
||||
|
||||
Logger.info(
|
||||
"[KillsPreloader] Phase 1 (quick) done => calls_count=#{state_after_quick.calls_count}, elapsed=#{time_quick_ms}ms"
|
||||
)
|
||||
|
||||
# ---- EXPANDED PASS ----
|
||||
state_expanded = %{state_after_quick | phase: :expanded_pass}
|
||||
|
||||
{time_expanded_ms, final_state} =
|
||||
measure_execution_time(fn ->
|
||||
do_pass(unique_systems, :expanded, @quick_hours, @expanded_limit, state_expanded)
|
||||
end)
|
||||
|
||||
Logger.info(
|
||||
"[KillsPreloader] Phase 2 (expanded) done => calls_count=#{final_state.calls_count}, elapsed=#{time_expanded_ms}ms"
|
||||
)
|
||||
|
||||
# Reset phase to :idle
|
||||
{:noreply, %{final_state | phase: :idle}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_other, state), do: {:noreply, state}
|
||||
|
||||
defp resolve_last_active_maps({:ok, []}) do
|
||||
Logger.warning("[KillsPreloader] No last-active maps found. Using fallback logic...")
|
||||
|
||||
case WandererApp.Maps.get_available_maps() do
|
||||
{:ok, []} ->
|
||||
Logger.error("[KillsPreloader] Fallback: get_available_maps returned zero maps!")
|
||||
[]
|
||||
|
||||
{:ok, maps} ->
|
||||
# pick the newest map by updated_at
|
||||
fallback_map = Enum.max_by(maps, & &1.updated_at, fn -> nil end)
|
||||
if fallback_map, do: [fallback_map], else: []
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_last_active_maps({:ok, maps}) when is_list(maps),
|
||||
do: maps
|
||||
|
||||
defp resolve_last_active_maps({:error, reason}) do
|
||||
Logger.error("[KillsPreloader] Could not load last-active maps => #{inspect(reason)}")
|
||||
[]
|
||||
end
|
||||
|
||||
defp get_active_maps_with_subscription(maps) do
|
||||
maps
|
||||
|> Enum.filter(fn map ->
|
||||
{:ok, is_subscription_active} = map.id |> WandererApp.Map.is_subscription_active?()
|
||||
is_subscription_active
|
||||
end)
|
||||
end
|
||||
|
||||
defp gather_visible_systems(maps) do
|
||||
maps
|
||||
|> Enum.flat_map(fn map_record ->
|
||||
the_map_id = Map.get(map_record, :map_id) || Map.get(map_record, :id)
|
||||
|
||||
case WandererApp.MapSystemRepo.get_visible_by_map(the_map_id) do
|
||||
{:ok, systems} ->
|
||||
Enum.map(systems, fn sys -> {the_map_id, sys.solar_system_id} end)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
"[KillsPreloader] get_visible_by_map failed => map_id=#{inspect(the_map_id)}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp do_pass(unique_systems, pass_type, hours, limit, state) do
|
||||
Logger.info(
|
||||
"[KillsPreloader] Starting #{pass_type} pass => #{length(unique_systems)} systems"
|
||||
)
|
||||
|
||||
{final_state, kills_map} =
|
||||
unique_systems
|
||||
|> Task.async_stream(
|
||||
fn {_map_id, system_id} ->
|
||||
fetch_kills_for_system(system_id, pass_type, hours, limit, state)
|
||||
end,
|
||||
max_concurrency: state.max_concurrency,
|
||||
timeout: pass_timeout_ms(pass_type)
|
||||
)
|
||||
|> Enum.reduce({state, %{}}, fn task_result, {acc_state, acc_map} ->
|
||||
reduce_task_result(pass_type, task_result, acc_state, acc_map)
|
||||
end)
|
||||
|
||||
if map_size(kills_map) > 0 do
|
||||
broadcast_all_kills(kills_map, pass_type)
|
||||
end
|
||||
|
||||
final_state
|
||||
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)
|
||||
|
||||
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
||||
limit: limit,
|
||||
force: false
|
||||
) do
|
||||
{:ok, kills, updated_state} ->
|
||||
{:ok, system_id, kills, updated_state}
|
||||
|
||||
{:error, reason, updated_state} ->
|
||||
Logger.warning(
|
||||
"[KillsPreloader] Quick fetch failed => system=#{system_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, reason, updated_state}
|
||||
end
|
||||
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)
|
||||
|
||||
with {:ok, kills_1h, updated_state} <-
|
||||
KillsProvider.Fetcher.fetch_kills_for_system(system_id, hours, state,
|
||||
limit: limit,
|
||||
force: true
|
||||
),
|
||||
{:ok, final_kills, final_state} <-
|
||||
maybe_fetch_more_if_needed(system_id, kills_1h, limit, updated_state) do
|
||||
{:ok, system_id, final_kills, final_state}
|
||||
else
|
||||
{:error, reason, updated_state} ->
|
||||
Logger.warning(
|
||||
"[KillsPreloader] Expanded fetch (#{hours}h) failed => system=#{system_id}, reason=#{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, reason, updated_state}
|
||||
end
|
||||
end
|
||||
|
||||
# If we got fewer kills than `limit` from the 1h fetch, top up from 24h
|
||||
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)
|
||||
|
||||
case KillsProvider.Fetcher.fetch_kills_for_system(system_id, @expanded_hours, state,
|
||||
limit: needed,
|
||||
force: true
|
||||
) do
|
||||
{:ok, _kills_24h, updated_state2} ->
|
||||
final_kills =
|
||||
KillsCache.fetch_cached_kills(system_id)
|
||||
|> Enum.take(limit)
|
||||
|
||||
{:ok, final_kills, updated_state2}
|
||||
|
||||
{:error, reason2, updated_state2} ->
|
||||
Logger.warning(
|
||||
"[KillsPreloader] #{@expanded_hours}h fetch failed => system=#{system_id}, reason=#{inspect(reason2)}"
|
||||
)
|
||||
|
||||
{:error, reason2, updated_state2}
|
||||
end
|
||||
else
|
||||
{:ok, kills_1h, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp reduce_task_result(pass_type, task_result, acc_state, acc_map) do
|
||||
case task_result do
|
||||
{:ok, {:ok, sys_id, kills, updated_state}} ->
|
||||
# Merge calls count from updated_state into acc_state
|
||||
new_state = merge_calls_count(acc_state, updated_state)
|
||||
new_map = Map.put(acc_map, sys_id, kills)
|
||||
{new_state, new_map}
|
||||
|
||||
{:ok, {:error, reason, updated_state}} ->
|
||||
log_failed_task(pass_type, reason)
|
||||
new_state = merge_calls_count(acc_state, updated_state)
|
||||
{new_state, acc_map}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[KillsPreloader] #{pass_type} fetch task crashed => #{inspect(reason)}")
|
||||
{acc_state, acc_map}
|
||||
end
|
||||
end
|
||||
|
||||
defp log_failed_task(:quick, reason),
|
||||
do: Logger.warning("[KillsPreloader] Quick fetch task failed => #{inspect(reason)}")
|
||||
|
||||
defp log_failed_task(:expanded, reason),
|
||||
do: Logger.error("[KillsPreloader] Expanded fetch task failed => #{inspect(reason)}")
|
||||
|
||||
defp broadcast_all_kills(kills_map, pass_type) do
|
||||
Logger.info(
|
||||
"[KillsPreloader] Broadcasting kills => #{map_size(kills_map)} systems (#{pass_type})"
|
||||
)
|
||||
|
||||
Phoenix.PubSub.broadcast!(
|
||||
WandererApp.PubSub,
|
||||
"zkb_preload",
|
||||
%{
|
||||
event: :detailed_kills_updated,
|
||||
payload: kills_map,
|
||||
fetch_type: pass_type
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp merge_calls_count(%{calls_count: c1} = st1, %{calls_count: c2}),
|
||||
do: %{st1 | calls_count: c1 + c2}
|
||||
|
||||
defp merge_calls_count(st1, _other),
|
||||
do: st1
|
||||
|
||||
defp pass_timeout_ms(:quick), do: :timer.minutes(2)
|
||||
defp pass_timeout_ms(:expanded), do: :timer.minutes(5)
|
||||
|
||||
defp measure_execution_time(fun) when is_function(fun, 0) do
|
||||
start = System.monotonic_time()
|
||||
result = fun.()
|
||||
finish = System.monotonic_time()
|
||||
ms = System.convert_time_unit(finish - start, :native, :millisecond)
|
||||
{ms, result}
|
||||
end
|
||||
end
|
||||
@@ -1,127 +1,29 @@
|
||||
defmodule WandererApp.Zkb.KillsProvider do
|
||||
@moduledoc false
|
||||
use Fresh
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Zkb.KillsProvider.Websocket
|
||||
|
||||
defstruct [:connected]
|
||||
|
||||
require Logger
|
||||
def handle_connect(status, headers, state),
|
||||
do: Websocket.handle_connect(status, headers, state)
|
||||
|
||||
@heartbeat_interval 1_000
|
||||
def handle_in(frame, state),
|
||||
do: Websocket.handle_in(frame, state)
|
||||
|
||||
def handle_connect(_status, _headers, state) do
|
||||
Logger.debug(fn ->
|
||||
"#{__MODULE__}: connected to kills stream"
|
||||
end)
|
||||
def handle_control(msg, state),
|
||||
do: Websocket.handle_control(msg, state)
|
||||
|
||||
handle_subscribe("killstream", %__MODULE__{state | connected: true})
|
||||
end
|
||||
def handle_info(msg, state),
|
||||
do: Websocket.handle_info(msg, state)
|
||||
|
||||
def handle_in({:text, frame}, state) do
|
||||
frame
|
||||
|> Jason.decode!()
|
||||
|> handle_websocket(state)
|
||||
end
|
||||
def handle_disconnect(code, reason, state),
|
||||
do: Websocket.handle_disconnect(code, reason, state)
|
||||
|
||||
def handle_control({:ping, _message}, state) do
|
||||
Process.send_after(self(), :heartbeat, @heartbeat_interval)
|
||||
{:ok, state}
|
||||
end
|
||||
def handle_error(err, state),
|
||||
do: Websocket.handle_error(err, state)
|
||||
|
||||
def handle_control(_event, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_info(:heartbeat, state) do
|
||||
payload =
|
||||
Jason.encode!(%{
|
||||
"action" => "pong"
|
||||
})
|
||||
|
||||
{:reply, {:text, payload}, state}
|
||||
end
|
||||
|
||||
def handle_info(_message, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_info(_message, _ws, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
defp handle_subscribe(channel, state) do
|
||||
Logger.debug(fn ->
|
||||
"#{__MODULE__} subscribe: #{inspect(channel, pretty: true)}"
|
||||
end)
|
||||
|
||||
payload =
|
||||
Jason.encode!(%{
|
||||
"action" => "sub",
|
||||
"channel" => channel
|
||||
})
|
||||
|
||||
{:reply, {:text, payload}, state}
|
||||
end
|
||||
|
||||
defp handle_websocket(message, state) do
|
||||
case message |> parse_message() do
|
||||
nil ->
|
||||
{:ok, state}
|
||||
|
||||
%{solar_system_id: solar_system_id, kill_time: kill_time} = _message ->
|
||||
case DateTime.diff(DateTime.utc_now(), kill_time, :hour) do
|
||||
0 ->
|
||||
WandererApp.Cache.incr("zkb_kills_#{solar_system_id}", 1,
|
||||
default: 0,
|
||||
ttl: :timer.hours(1)
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_disconnect(1002, reason, _state) do
|
||||
Logger.warning(fn ->
|
||||
"Connection to socket lost by #{inspect(reason, pretty: true)}; reconnecting..."
|
||||
end)
|
||||
|
||||
:reconnect
|
||||
end
|
||||
|
||||
def handle_disconnect(_code, reason, _state) do
|
||||
Logger.warning(fn ->
|
||||
"Connection to socket lost by #{inspect(reason, pretty: true)}; closing..."
|
||||
end)
|
||||
|
||||
:reconnect
|
||||
end
|
||||
|
||||
def handle_error({error, _reason}, state)
|
||||
when error in [:encoding_failed, :casting_failed],
|
||||
do: {:ignore, state}
|
||||
|
||||
def handle_error(_error, _state), do: :reconnect
|
||||
|
||||
def handle_terminate(reason, _state) do
|
||||
Logger.warning(fn -> "Terminating client process with reason : #{inspect(reason)}" end)
|
||||
end
|
||||
|
||||
defp parse_message(
|
||||
%{
|
||||
"solar_system_id" => solar_system_id,
|
||||
"killmail_time" => killmail_time
|
||||
} = _message
|
||||
) do
|
||||
{:ok, kill_time, _} = DateTime.from_iso8601(killmail_time)
|
||||
|
||||
%{
|
||||
solar_system_id: solar_system_id,
|
||||
kill_time: kill_time
|
||||
}
|
||||
end
|
||||
|
||||
defp parse_message(_message), do: nil
|
||||
def handle_terminate(reason, state),
|
||||
do: Websocket.handle_terminate(reason, state)
|
||||
end
|
||||
|
||||
@@ -8,15 +8,27 @@ defmodule WandererApp.Zkb.Supervisor do
|
||||
end
|
||||
|
||||
def init(_init_args) do
|
||||
children = [
|
||||
{WandererApp.Zkb.KillsProvider,
|
||||
uri: "wss://zkillboard.com/websocket/",
|
||||
state: %WandererApp.Zkb.KillsProvider{},
|
||||
opts: [
|
||||
name: {:local, :zkb_kills_provider},
|
||||
mint_upgrade_opts: [Mint.WebSocket.PerMessageDeflate]
|
||||
]}
|
||||
]
|
||||
preloader_child =
|
||||
unless WandererApp.Env.zkill_preload_disabled?() do
|
||||
{WandererApp.Zkb.KillsPreloader, []}
|
||||
end
|
||||
|
||||
children =
|
||||
[
|
||||
{
|
||||
WandererApp.Zkb.KillsProvider,
|
||||
uri: "wss://zkillboard.com/websocket/",
|
||||
state: %WandererApp.Zkb.KillsProvider{
|
||||
connected: false
|
||||
},
|
||||
opts: [
|
||||
name: {:local, :zkb_kills_provider},
|
||||
mint_upgrade_opts: [Mint.WebSocket.PerMessageDeflate]
|
||||
]
|
||||
},
|
||||
preloader_child
|
||||
]
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user