mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-04 14:55:34 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4af12c21b2 | ||
|
|
497da1e5f7 | ||
|
|
5bd968acae | ||
|
|
f74c20142c | ||
|
|
d4c40d7542 | ||
|
|
04f3fec0c0 | ||
|
|
cd0b4b0fc9 | ||
|
|
e7b115e6e6 | ||
|
|
dff8fc6396 | ||
|
|
afdaeb3d34 | ||
|
|
ac6053361e | ||
|
|
eb3e1ba3aa | ||
|
|
8468a9b5de | ||
|
|
5eafe59dcb | ||
|
|
b38bcaa8cf | ||
|
|
8a238a447d | ||
|
|
3731219216 | ||
|
|
73d5fd5f67 | ||
|
|
e8e4aed6d5 | ||
|
|
63571a462f | ||
|
|
606add4142 | ||
|
|
dac480b059 | ||
|
|
5f67cb1dd7 | ||
|
|
5886fff753 | ||
|
|
da2e12bdd1 | ||
|
|
05c3d20e56 | ||
|
|
4633d26517 | ||
|
|
30b0556d47 | ||
|
|
e094378dc5 | ||
|
|
0c48189503 | ||
|
|
a5c346627a | ||
|
|
4e526040bf | ||
|
|
869c25cd60 | ||
|
|
6aac698cd8 | ||
|
|
230016b90f | ||
|
|
4b1aef8dd9 | ||
|
|
d34509d7a0 | ||
|
|
fca98ec232 | ||
|
|
e2814e95bd | ||
|
|
68a3f84704 | ||
|
|
4bc76feefc | ||
|
|
da39a55fd0 | ||
|
|
ee3cf04cd4 | ||
|
|
d79e7fe2ff | ||
|
|
8de9fdef32 | ||
|
|
f51deeec2d | ||
|
|
a971c69a96 | ||
|
|
b7995f50de | ||
|
|
14997a2959 | ||
|
|
8fef6bcf82 | ||
|
|
1f82d23963 | ||
|
|
28317a2431 | ||
|
|
6aac496a57 | ||
|
|
ac9306b713 | ||
|
|
d55e804efa | ||
|
|
08407a5679 | ||
|
|
c37d175bec | ||
|
|
69c5326e72 | ||
|
|
305f63e11d | ||
|
|
698fd5e083 | ||
|
|
1af8342d30 | ||
|
|
68b59da78e | ||
|
|
e784a3f850 | ||
|
|
a45e2f3fc2 | ||
|
|
8a3d920c31 | ||
|
|
996d7c47bd | ||
|
|
8d2b9db430 | ||
|
|
423ce343c7 | ||
|
|
1c17912d9f | ||
|
|
6714eb5d9b | ||
|
|
1620e1fd21 | ||
|
|
859014874f | ||
|
|
ef44881f06 | ||
|
|
b0532325fa | ||
|
|
2c00bd426e | ||
|
|
6eccf2ac67 |
@@ -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"
|
||||
|
||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -78,22 +78,23 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-elixir-deps
|
||||
with:
|
||||
path: deps
|
||||
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
|
||||
path: |
|
||||
deps
|
||||
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ env.cache-name }}-
|
||||
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
|
||||
- name: 😅 Cache compiled build
|
||||
id: cache-build
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-compiled-build
|
||||
with:
|
||||
path: |
|
||||
**/_build
|
||||
_build
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
|
||||
@@ -187,6 +188,8 @@ jobs:
|
||||
push: true
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
|
||||
@@ -258,17 +261,17 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create --tag ${{ env.REGISTRY_IMAGE }}:latest \
|
||||
${{ env.REGISTRY_IMAGE }}:${{ needs.docker.outputs.release-tag }} \
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ needs.docker.outputs.release-tag }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
create-release:
|
||||
name: 🏷 Create Release
|
||||
|
||||
269
CHANGELOG.md
269
CHANGELOG.md
@@ -2,6 +2,275 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Widgets: Fix widgets not visible on map
|
||||
|
||||
## [v1.43.5](https://github.com/wanderer-industries/wanderer/compare/v1.43.4...v1.43.5) (2025-01-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Audit: Fix signature added/removed system name
|
||||
|
||||
## [v1.43.4](https://github.com/wanderer-industries/wanderer/compare/v1.43.3...v1.43.4) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* improve structure widget styling (#127)
|
||||
|
||||
## [v1.43.3](https://github.com/wanderer-industries/wanderer/compare/v1.43.2...v1.43.3) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.2](https://github.com/wanderer-industries/wanderer/compare/v1.43.1...v1.43.2) (2025-01-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* prevent constraint error for follow/toggle (#132)
|
||||
|
||||
## [v1.43.1](https://github.com/wanderer-industries/wanderer/compare/v1.43.0...v1.43.1) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.43.0](https://github.com/wanderer-industries/wanderer/compare/v1.42.5...v1.43.0) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add news post for structures widget (#131)
|
||||
|
||||
## [v1.42.5](https://github.com/wanderer-industries/wanderer/compare/v1.42.4...v1.42.5) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix link signatures on splash. Fix deleting connection on locked system remove.
|
||||
|
||||
## [v1.42.4](https://github.com/wanderer-industries/wanderer/compare/v1.42.3...v1.42.4) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fix system statics list (required EVE DB data update). Add system name to signature added/removed audit log
|
||||
|
||||
## [v1.42.3](https://github.com/wanderer-industries/wanderer/compare/v1.42.2...v1.42.3) (2025-01-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* change structure tooltip to avoid paste confusion (#125)
|
||||
|
||||
* change structure tooltip to avoid paste confusion
|
||||
|
||||
* clarify use of evetime and use primereact calendar
|
||||
|
||||
## [v1.42.2](https://github.com/wanderer-industries/wanderer/compare/v1.42.1...v1.42.2) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.42.1](https://github.com/wanderer-industries/wanderer/compare/v1.42.0...v1.42.1) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Remove linked sig ID if system containing signature removed from map
|
||||
|
||||
## [v1.42.0](https://github.com/wanderer-industries/wanderer/compare/v1.41.0...v1.42.0) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Audit: Add 'Signatures added/removed' map audit events
|
||||
|
||||
## [v1.41.0](https://github.com/wanderer-industries/wanderer/compare/v1.40.7...v1.41.0) (2025-01-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Audit: Add 'ACL added/removed' map audit events
|
||||
|
||||
## [v1.40.7](https://github.com/wanderer-industries/wanderer/compare/v1.40.6...v1.40.7) (2025-01-15)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.40.6](https://github.com/wanderer-industries/wanderer/compare/v1.40.5...v1.40.6) (2025-01-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix follow mode
|
||||
|
||||
* center system is not selected text for structures (#122)
|
||||
|
||||
* Map: Fix system revert issues
|
||||
|
||||
* Map: Fix issues with splashing signatures select & sig ID in temp names
|
||||
|
||||
## [v1.40.5](https://github.com/wanderer-industries/wanderer/compare/v1.40.4...v1.40.5) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix follow mode
|
||||
|
||||
## [v1.40.4](https://github.com/wanderer-industries/wanderer/compare/v1.40.3...v1.40.4) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* center system is not selected text for structures (#122)
|
||||
|
||||
## [v1.40.3](https://github.com/wanderer-industries/wanderer/compare/v1.40.2...v1.40.3) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix system revert issues
|
||||
|
||||
## [v1.40.2](https://github.com/wanderer-industries/wanderer/compare/v1.40.1...v1.40.2) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix issues with splashing signatures select & sig ID in temp names
|
||||
|
||||
## [v1.40.1](https://github.com/wanderer-industries/wanderer/compare/v1.40.0...v1.40.1) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.40.0](https://github.com/wanderer-industries/wanderer/compare/v1.39.3...v1.40.0) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add structure widget with timer and associated api
|
||||
|
||||
## [v1.39.3](https://github.com/wanderer-industries/wanderer/compare/v1.39.2...v1.39.3) (2025-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add style of corners for windows. Add ability to reset widgets. A lot of refactoring
|
||||
|
||||
## [v1.39.2](https://github.com/wanderer-industries/wanderer/compare/v1.39.1...v1.39.2) (2025-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.39.1](https://github.com/wanderer-industries/wanderer/compare/v1.39.0...v1.39.1) (2025-01-13)
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
MiniMap,
|
||||
Node,
|
||||
NodeChange,
|
||||
@@ -23,12 +23,10 @@ import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
SolarSystemEdge,
|
||||
SolarSystemNodeDefault,
|
||||
SolarSystemNodeTheme,
|
||||
useContextMenuConnectionHandlers,
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { wrapNode } from './utils/wrapNode';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
@@ -77,9 +75,6 @@ const initialEdges = [
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
const edgeTypes = {
|
||||
floating: SolarSystemEdge,
|
||||
};
|
||||
@@ -89,6 +84,7 @@ interface MapCompProps {
|
||||
onCommand: OutCommandHandler;
|
||||
onSelectionChange: OnMapSelectionChange;
|
||||
onManualDelete(systems: string[]): void;
|
||||
canRemoveConnection?(connectionId: string): boolean;
|
||||
onConnectionInfoClick?(e: SolarSystemConnection): void;
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onSelectionContextMenu?: NodeSelectionMouseHandler;
|
||||
@@ -118,28 +114,26 @@ const MapComp = ({
|
||||
isSoftBackground,
|
||||
theme,
|
||||
onAddSystem,
|
||||
canRemoveConnection,
|
||||
}: MapCompProps) => {
|
||||
const { getNode, getNodes } = useReactFlow();
|
||||
const { getEdge, getNode, getNodes } = useReactFlow();
|
||||
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
|
||||
|
||||
|
||||
const nodeTypes = useMemo(() => {
|
||||
return {
|
||||
custom:
|
||||
theme !== '' && theme !== 'default'
|
||||
? wrapNode(SolarSystemNodeTheme)
|
||||
: wrapNode(SolarSystemNodeDefault),
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
|
||||
useMapHandlers(refn, onSelectionChange);
|
||||
useUpdateNodes(nodes);
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color } = 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,
|
||||
};
|
||||
}, [nodeComponent]);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
params => {
|
||||
@@ -228,7 +222,41 @@ const MapComp = ({
|
||||
|
||||
onNodesChange(nextChanges);
|
||||
},
|
||||
[getNode, onManualDelete, onNodesChange],
|
||||
[getNode, getNodes, onManualDelete, onNodesChange],
|
||||
);
|
||||
|
||||
const handleEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => {
|
||||
const nextChanges = changes.reduce((acc, change) => {
|
||||
if (change.type !== 'remove') {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (canRemoveConnection?.(change.id)) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const edge = getEdge(change.id);
|
||||
if (!edge) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
const sourceNode = getNode(edge.source);
|
||||
const targetNode = getNode(edge.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return [...acc, change];
|
||||
}
|
||||
|
||||
if (sourceNode.data.locked || targetNode.data.locked) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, change];
|
||||
}, [] as EdgeChange[]);
|
||||
|
||||
onEdgesChange(nextChanges);
|
||||
},
|
||||
[getEdge, getNode, onEdgesChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -246,14 +274,14 @@ const MapComp = ({
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={onConnect}
|
||||
// TODO we need save into session all of this
|
||||
// and on any action do either
|
||||
defaultViewport={getViewPortFromStore()}
|
||||
edgeTypes={edgeTypes}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
connectionMode={connectionMode}
|
||||
snapToGrid
|
||||
nodeDragThreshold={10}
|
||||
onNodeDragStop={handleDragStop}
|
||||
@@ -286,6 +314,12 @@ const MapComp = ({
|
||||
maxZoom={1.5}
|
||||
elevateNodesOnSelect
|
||||
deleteKeyCode={['Delete']}
|
||||
{...(isPanAndDrag
|
||||
? {
|
||||
selectionOnDrag: true,
|
||||
panOnDrag: [2],
|
||||
}
|
||||
: {})}
|
||||
// TODO need create clear example with problem with that flag
|
||||
// if system is not visible edge not drawing (and any render in Custom node is not happening)
|
||||
// onlyRenderVisibleElements
|
||||
|
||||
@@ -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 & {
|
||||
@@ -9,6 +9,7 @@ export type MapData = MapUnionTypes & {
|
||||
visibleNodes: Set<string>;
|
||||
showKSpaceBG: boolean;
|
||||
isThickConnections: boolean;
|
||||
linkedSigEveId: string;
|
||||
};
|
||||
|
||||
interface MapProviderProps {
|
||||
@@ -29,10 +30,13 @@ 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>,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
|
||||
@@ -6,7 +6,14 @@ $pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020; // Dark background for tooltips
|
||||
$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;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import classes from './SolarSystemNodeDefault.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
|
||||
import {
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
@@ -15,64 +13,35 @@ import {
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
|
||||
export const SolarSystemNodeDefault = memo(props => {
|
||||
const {
|
||||
charactersInSystem,
|
||||
classTitle,
|
||||
classTitleColor,
|
||||
customName,
|
||||
effectName,
|
||||
hasUserCharacters,
|
||||
hubs,
|
||||
visible,
|
||||
labelCustom,
|
||||
labelsInfo,
|
||||
locked,
|
||||
isShattered,
|
||||
isThickConnections,
|
||||
isWormhole,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
regionClass,
|
||||
regionName,
|
||||
status,
|
||||
selected,
|
||||
tag,
|
||||
showHandlers,
|
||||
systemName,
|
||||
sortedStatics,
|
||||
solarSystemId,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
dbClick: handleDbClick,
|
||||
} = useSolarSystemNode(props);
|
||||
export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
{nodeVars.visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{labelCustom !== '' && (
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShattered && (
|
||||
{nodeVars.isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
|
||||
{nodeVars.killsCount && (
|
||||
<div 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)}>{killsCount}</span>
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{labelsInfo.map(x => (
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
@@ -81,19 +50,30 @@ export const SolarSystemNodeDefault = memo(props => {
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
|
||||
[classes.selected]: selected,
|
||||
})}
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{visible && (
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
|
||||
{classTitle ?? '-'}
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classTitle,
|
||||
nodeVars.classTitleColor,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
|
||||
)}
|
||||
>
|
||||
{nodeVars.classTitle ?? '-'}
|
||||
</div>
|
||||
|
||||
{tag != null && tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{tag}</div>
|
||||
{nodeVars.tag != null && nodeVars.tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle, 'text-sky-400 font-medium')}>{nodeVars.tag}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
@@ -102,53 +82,55 @@ export const SolarSystemNodeDefault = memo(props => {
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
|
||||
)}
|
||||
>
|
||||
{systemName}
|
||||
{nodeVars.systemName}
|
||||
</div>
|
||||
|
||||
{isWormhole && (
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{sortedStatics.map(whClass => (
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectName !== null && isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
|
||||
{nodeVars.effectName !== null && nodeVars.isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{customName && (
|
||||
{nodeVars.customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-blue-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{customName}
|
||||
{nodeVars.customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isWormhole && !customName && (
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
<div className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] text-stone-300 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
{regionName}
|
||||
{nodeVars.regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isWormhole && !customName && <div />}
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{hubs.includes(solarSystemId.toString()) && (
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{charactersInSystem.length > 0 && (
|
||||
{nodeVars.charactersInSystem.length > 0 && (
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
['text-amber-300']: hasUserCharacters,
|
||||
['text-amber-300']: nodeVars.hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
||||
<span className="font-sans">{charactersInSystem.length}</span>
|
||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -158,19 +140,19 @@ export const SolarSystemNodeDefault = memo(props => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{visible && (
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
{unsplashedLeft.length > 0 && (
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{unsplashedLeft.map(sig => (
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unsplashedRight.length > 0 && (
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{unsplashedRight.map(sig => (
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
@@ -178,44 +160,44 @@ export const SolarSystemNodeDefault = memo(props => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
|
||||
@@ -1,407 +1,91 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@import './SolarSystemNodeDefault.module.scss';
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: #d291bc;
|
||||
$pastel-green: #88b04b;
|
||||
$pastel-yellow: #ffdd59;
|
||||
$dark-bg: #2d2d2d;
|
||||
$text-color: #ffffff;
|
||||
$tooltip-bg: #202020; // Dark background for tooltips
|
||||
/* ---------------------------
|
||||
Only override what's different
|
||||
--------------------------- */
|
||||
|
||||
/* 1) .RootCustomNode:
|
||||
- new background-color using CSS var
|
||||
- plus color, font-family, and font-weight */
|
||||
.RootCustomNode {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
height: 34px;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
|
||||
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;
|
||||
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&.Mataria,
|
||||
&.Amarria,
|
||||
&.Gallente,
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Mataria {
|
||||
&::before {
|
||||
background-image: url('/images/mataria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
background-image: url('/images/caldaria-180.png');
|
||||
opacity: 0.6;
|
||||
background-position-x: 1px;
|
||||
background-position-y: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Amarria {
|
||||
&::before {
|
||||
opacity: 0.45;
|
||||
background-image: url('/images/amarr-180.png');
|
||||
background-position-x: 0;
|
||||
background-position-y: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Gallente {
|
||||
&::before {
|
||||
opacity: 0.5;
|
||||
background-image: url('/images/gallente-180.png');
|
||||
background-position-x: 1px;
|
||||
background-position-y: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
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(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-home),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-home);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-friendly {
|
||||
border: 1px solid var(--eve-solar-system-status-color-friendly-dark20);
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-friendly-dark30),
|
||||
transparent
|
||||
);
|
||||
&.selected {
|
||||
border-color: var(--eve-solar-system-status-color-friendly-dark5);
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-lookingFor {
|
||||
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
|
||||
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
}
|
||||
|
||||
&.eve-system-status-warning {
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-warning),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&.eve-system-status-dangerous {
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-dangerous),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&.eve-system-status-target {
|
||||
background-image: linear-gradient(
|
||||
275deg,
|
||||
var(--eve-solar-system-status-target),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* 2) .Bookmarks:
|
||||
- add var-based font family/weight
|
||||
*/
|
||||
.Bookmarks {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
left: 4px;
|
||||
|
||||
& > .Bookmark {
|
||||
min-width: 13px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
top: -13px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
|
||||
//background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.BookmarkWithIcon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -2px;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 1);
|
||||
padding-right: 2px;
|
||||
|
||||
& > .icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
& > .text {
|
||||
margin-top: 1px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Unsplashed {
|
||||
position: absolute;
|
||||
width: calc(50% - 4px);
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
left: 2px;
|
||||
|
||||
&--right {
|
||||
left: calc(50% + 6px);
|
||||
}
|
||||
|
||||
& > .Signature {
|
||||
width: 13px;
|
||||
height: 4px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
font-weight: bolder;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
display: block;
|
||||
|
||||
background-color: #833ca4;
|
||||
|
||||
&:not(:first-child) {
|
||||
box-shadow: inset 4px -3px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
font-size: 8px;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
|
||||
.classTitle {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 73%);
|
||||
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);
|
||||
|
||||
color: var(--rf-tag-color, #38BDF8);
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
.classSystemName {
|
||||
font-family: inherit !important;
|
||||
font-weight: bold;
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.classSystemName {
|
||||
font-family: inherit !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.solarSystemName {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
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)
|
||||
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)
|
||||
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;
|
||||
//align-items: center;
|
||||
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);
|
||||
}
|
||||
|
||||
& > i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
font-size: 9px;
|
||||
line-height: 9px;
|
||||
font-weight: 500;
|
||||
//margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.effect {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -2px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.statics {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
font-size: 8px;
|
||||
|
||||
& > * {
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
/* Firefox kostyl */
|
||||
@-moz-document url-prefix() {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.Handlers {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.Handle {
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
border: 1px solid $pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
}
|
||||
|
||||
&.HandleTop {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -2px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -2px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -2px;
|
||||
}
|
||||
|
||||
&.Tick {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
|
||||
&.HandleTop {
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
&.HandleRight {
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
&.HandleBottom {
|
||||
bottom: -3px;
|
||||
}
|
||||
|
||||
&.HandleLeft {
|
||||
left: -3px;
|
||||
font-family: var(--rf-node-font-family, inherit) !important;
|
||||
font-weight: var(--rf-node-font-weight, inherit) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { MapSolarSystemType } from '../../map.types';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import classes from './SolarSystemNodeTheme.module.scss';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { useSolarSystemNode } from '../../hooks/useSolarSystemNode';
|
||||
|
||||
import {
|
||||
MARKER_BOOKMARK_BG_STYLES,
|
||||
STATUS_CLASSES,
|
||||
@@ -15,64 +13,35 @@ import {
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
|
||||
export const SolarSystemNodeTheme = memo(props => {
|
||||
const {
|
||||
charactersInSystem,
|
||||
classTitle,
|
||||
classTitleColor,
|
||||
customName,
|
||||
effectName,
|
||||
hasUserCharacters,
|
||||
hubs,
|
||||
visible,
|
||||
labelCustom,
|
||||
labelsInfo,
|
||||
locked,
|
||||
isShattered,
|
||||
isThickConnections,
|
||||
isWormhole,
|
||||
killsCount,
|
||||
killsActivityType,
|
||||
regionClass,
|
||||
regionName,
|
||||
status,
|
||||
selected,
|
||||
tag,
|
||||
showHandlers,
|
||||
systemName,
|
||||
sortedStatics,
|
||||
solarSystemId,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
dbClick: handleDbClick,
|
||||
} = useSolarSystemNode(props);
|
||||
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
|
||||
const nodeVars = useSolarSystemNode(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
{nodeVars.visible && (
|
||||
<div className={classes.Bookmarks}>
|
||||
{labelCustom !== '' && (
|
||||
{nodeVars.labelCustom !== '' && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.custom)}>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{labelCustom}</span>
|
||||
<span className="[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] ">{nodeVars.labelCustom}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShattered && (
|
||||
{nodeVars.isShattered && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES.shattered)}>
|
||||
<span className={clsx('pi pi-chart-pie', classes.icon)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{killsCount && (
|
||||
<div className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[killsActivityType!])}>
|
||||
{nodeVars.killsCount && (
|
||||
<div 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)}>{killsCount}</span>
|
||||
<span className={clsx(classes.text)}>{nodeVars.killsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{labelsInfo.map(x => (
|
||||
{nodeVars.labelsInfo.map(x => (
|
||||
<div key={x.id} className={clsx(classes.Bookmark, MARKER_BOOKMARK_BG_STYLES[x.id])}>
|
||||
{x.shortName}
|
||||
</div>
|
||||
@@ -81,18 +50,31 @@ export const SolarSystemNodeTheme = memo(props => {
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(classes.RootCustomNode, regionClass && classes[regionClass], classes[STATUS_CLASSES[status]], {
|
||||
[classes.selected]: selected,
|
||||
})}
|
||||
className={clsx(
|
||||
classes.RootCustomNode,
|
||||
nodeVars.regionClass && classes[nodeVars.regionClass],
|
||||
classes[STATUS_CLASSES[nodeVars.status]],
|
||||
{
|
||||
[classes.selected]: nodeVars.selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{visible && (
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
<div className={clsx(classes.classTitle, classTitleColor, '[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]')}>
|
||||
{classTitle ?? '-'}
|
||||
<div
|
||||
className={clsx(
|
||||
classes.classTitle,
|
||||
nodeVars.classTitleColor,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]',
|
||||
)}
|
||||
>
|
||||
{nodeVars.classTitle ?? '-'}
|
||||
</div>
|
||||
|
||||
{tag != null && tag !== '' && <div className={clsx(classes.TagTitle)}>{tag}</div>}
|
||||
{nodeVars.tag != null && nodeVars.tag !== '' && (
|
||||
<div className={clsx(classes.TagTitle)}>{nodeVars.tag}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -100,63 +82,65 @@ export const SolarSystemNodeTheme = memo(props => {
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap',
|
||||
)}
|
||||
>
|
||||
{systemName}
|
||||
{nodeVars.systemName}
|
||||
</div>
|
||||
|
||||
{isWormhole && (
|
||||
{nodeVars.isWormhole && (
|
||||
<div className={classes.statics}>
|
||||
{sortedStatics.map(whClass => (
|
||||
{nodeVars.sortedStatics.map(whClass => (
|
||||
<WormholeClassComp key={whClass} id={whClass} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectName !== null && isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[effectName])} />
|
||||
{nodeVars.effectName !== null && nodeVars.isWormhole && (
|
||||
<div className={clsx(classes.effect, EFFECT_BACKGROUND_STYLES[nodeVars.effectName])} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx(classes.BottomRow, 'flex items-center justify-between')}>
|
||||
{customName && (
|
||||
{nodeVars.customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CustomName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
{customName}
|
||||
{nodeVars.customName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isWormhole && !customName && (
|
||||
{!nodeVars.isWormhole && !nodeVars.customName && (
|
||||
<div
|
||||
className={clsx(
|
||||
classes.RegionName,
|
||||
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] whitespace-nowrap overflow-hidden text-ellipsis mr-0.5',
|
||||
)}
|
||||
>
|
||||
{regionName}
|
||||
{nodeVars.regionName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isWormhole && !customName && <div />}
|
||||
{nodeVars.isWormhole && !nodeVars.customName && <div />}
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex gap-1 items-center">
|
||||
{locked && <i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />}
|
||||
{nodeVars.locked && (
|
||||
<i className={PrimeIcons.LOCK} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{hubs.includes(solarSystemId.toString()) && (
|
||||
{nodeVars.hubs.includes(nodeVars.solarSystemId.toString()) && (
|
||||
<i className={PrimeIcons.MAP_MARKER} style={{ fontSize: '0.45rem', fontWeight: 'bold' }} />
|
||||
)}
|
||||
|
||||
{charactersInSystem.length > 0 && (
|
||||
{nodeVars.charactersInSystem.length > 0 && (
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
[classes.hasUserCharacters]: hasUserCharacters,
|
||||
[classes.hasUserCharacters]: nodeVars.hasUserCharacters,
|
||||
})}
|
||||
>
|
||||
<i className="pi pi-users" style={{ fontSize: '0.50rem' }} />
|
||||
<span className="font-sans">{charactersInSystem.length}</span>
|
||||
<span className="font-sans">{nodeVars.charactersInSystem.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -166,19 +150,19 @@ export const SolarSystemNodeTheme = memo(props => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{visible && (
|
||||
{nodeVars.visible && (
|
||||
<>
|
||||
{unsplashedLeft.length > 0 && (
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{unsplashedLeft.map(sig => (
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unsplashedRight.length > 0 && (
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{unsplashedRight.map(sig => (
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.sig_id} signature={sig} />
|
||||
))}
|
||||
</div>
|
||||
@@ -186,44 +170,44 @@ export const SolarSystemNodeTheme = memo(props => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div onMouseDownCapture={handleDbClick} className={classes.Handlers}>
|
||||
<div onMouseDownCapture={nodeVars.dbClick} className={classes.Handlers}>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleTop, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Top}
|
||||
id="a"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleRight, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleBottom, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Bottom}
|
||||
id="c"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
className={clsx(classes.Handle, classes.HandleLeft, {
|
||||
[classes.selected]: selected,
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.selected]: nodeVars.selected,
|
||||
[classes.Tick]: nodeVars.isThickConnections,
|
||||
})}
|
||||
style={{ visibility: showHandlers ? 'visible' : 'hidden' }}
|
||||
style={{ visibility: nodeVars.showHandlers ? 'visible' : 'hidden' }}
|
||||
position={Position.Left}
|
||||
id="d"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SolarSystemNodeDefault, SolarSystemNodeTheme } from '../components/SolarSystemNode';
|
||||
import type { NodeProps } from 'reactflow';
|
||||
import type { ComponentType } from 'react';
|
||||
import { MapSolarSystemType } from '../map.types';
|
||||
import { ConnectionMode } from 'reactflow';
|
||||
|
||||
export type SolarSystemNodeComponent = ComponentType<NodeProps<MapSolarSystemType>>;
|
||||
|
||||
interface ThemeBehavior {
|
||||
isPanAndDrag: boolean;
|
||||
nodeComponent: SolarSystemNodeComponent;
|
||||
connectionMode: ConnectionMode;
|
||||
}
|
||||
|
||||
const THEME_BEHAVIORS: {
|
||||
[key: string]: ThemeBehavior;
|
||||
} = {
|
||||
default: {
|
||||
isPanAndDrag: false,
|
||||
nodeComponent: SolarSystemNodeDefault,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
},
|
||||
pathfinder: {
|
||||
isPanAndDrag: true,
|
||||
nodeComponent: SolarSystemNodeTheme,
|
||||
connectionMode: ConnectionMode.Loose,
|
||||
},
|
||||
};
|
||||
|
||||
export function getBehaviorForTheme(themeName: string) {
|
||||
return THEME_BEHAVIORS[themeName] ?? THEME_BEHAVIORS.default;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
|
||||
break;
|
||||
case Commands.updateSystems:
|
||||
setTimeout(() => mapUpdateSystems(data as CommandUpdateSystems), 100);
|
||||
mapUpdateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems:
|
||||
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
|
||||
@@ -70,7 +70,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
setTimeout(() => addConnections(data as CommandAddConnections), 100);
|
||||
break;
|
||||
case Commands.removeConnections:
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
|
||||
break;
|
||||
case Commands.charactersUpdated:
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { MapSolarSystemType } from '../map.types';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider';
|
||||
@@ -9,7 +10,7 @@ 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 { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, OutCommand } from '@/hooks/Mapper/types';
|
||||
import { LABELS_INFO, LABELS_ORDER } from '@/hooks/Mapper/components/map/constants';
|
||||
|
||||
function getActivityType(count: number) {
|
||||
@@ -30,8 +31,8 @@ function sortedLabels(labels: string[]) {
|
||||
return LABELS_ORDER.filter(x => labels.includes(x)).map(x => LABELS_INFO[x]);
|
||||
}
|
||||
|
||||
export function useSolarSystemNode(props: any) {
|
||||
const { data, selected, id } = props;
|
||||
export function useSolarSystemNode(props: NodeProps<MapSolarSystemType>) {
|
||||
const { id, data, selected } = props;
|
||||
const {
|
||||
system_static_info,
|
||||
system_signatures,
|
||||
@@ -57,7 +58,6 @@ export function useSolarSystemNode(props: any) {
|
||||
solar_system_name,
|
||||
} = system_static_info;
|
||||
|
||||
// Global map state
|
||||
const {
|
||||
interfaceSettings,
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
@@ -85,7 +85,6 @@ export function useSolarSystemNode(props: any) {
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
|
||||
// logic
|
||||
const visible = useMemo(() => visibleNodes.has(id), [id, visibleNodes]);
|
||||
|
||||
const systemSignatures = useMemo(
|
||||
@@ -142,12 +141,12 @@ export function useSolarSystemNode(props: any) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isShowLinkedSigIdTempName) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : linkedSigPrefix;
|
||||
if (isShowLinkedSigIdTempName && linkedSigPrefix) {
|
||||
return temporary_name ? `${linkedSigPrefix}・${temporary_name}` : `${linkedSigPrefix}・${solar_system_name}`;
|
||||
}
|
||||
|
||||
return temporary_name;
|
||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, temporary_name]);
|
||||
}, [isShowLinkedSigIdTempName, isTempSystemNameEnabled, linkedSigPrefix, solar_system_name, temporary_name]);
|
||||
|
||||
const systemName = useMemo(() => {
|
||||
if (isTempSystemNameEnabled && temporaryName) {
|
||||
@@ -156,7 +155,7 @@ export function useSolarSystemNode(props: any) {
|
||||
return solar_system_name;
|
||||
}, [isTempSystemNameEnabled, solar_system_name, temporaryName]);
|
||||
|
||||
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name);
|
||||
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name) || null;
|
||||
|
||||
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
|
||||
if (!isShowUnsplashedSignatures) {
|
||||
@@ -174,10 +173,9 @@ export function useSolarSystemNode(props: any) {
|
||||
}, [isShowUnsplashedSignatures, systemSignatures]);
|
||||
|
||||
const nodeVars = {
|
||||
// original props
|
||||
id,
|
||||
selected,
|
||||
// computed
|
||||
|
||||
visible,
|
||||
isWormhole,
|
||||
classTitleColor,
|
||||
@@ -214,3 +212,43 @@ export function useSolarSystemNode(props: any) {
|
||||
|
||||
return nodeVars;
|
||||
}
|
||||
|
||||
export interface SolarSystemNodeVars {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
visible: boolean;
|
||||
isWormhole: boolean;
|
||||
classTitleColor: string | null;
|
||||
killsCount: number | null;
|
||||
killsActivityType: string | null;
|
||||
hasUserCharacters: boolean;
|
||||
showHandlers: boolean;
|
||||
regionClass: string | null;
|
||||
systemName: string;
|
||||
customName?: string | null;
|
||||
labelCustom: string | null;
|
||||
isShattered: boolean;
|
||||
tag?: string | null;
|
||||
status?: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
labelsInfo: Array<any>;
|
||||
dbClick: (event?: void) => void;
|
||||
sortedStatics: Array<string | number>;
|
||||
effectName: string | null;
|
||||
regionName: string | null;
|
||||
solarSystemId: number;
|
||||
solarSystemName: string | null;
|
||||
locked: boolean;
|
||||
hubs: string[] | number[];
|
||||
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>;
|
||||
isThickConnections: boolean;
|
||||
classTitle: string | null;
|
||||
temporaryName?: string | null;
|
||||
}
|
||||
|
||||
@@ -27,5 +27,5 @@
|
||||
--text-color: #ffffff;
|
||||
--tooltip-bg: #202020;
|
||||
|
||||
|
||||
--window-corner: #72716f;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
@import './eve-common-variables';
|
||||
@import './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
@@ -40,7 +39,6 @@
|
||||
--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;
|
||||
@@ -48,4 +46,6 @@
|
||||
|
||||
--rf-tag-color: #fbbf24;
|
||||
--rf-has-user-characters: #5cb85c;
|
||||
}
|
||||
|
||||
--window-corner: #72716f;
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// wrapNode.ts
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { SolarSystemNodeProps } from '../components/SolarSystemNode';
|
||||
|
||||
export function wrapNode<T>(
|
||||
SolarSystemNode: React.FC<SolarSystemNodeProps<T>>
|
||||
): React.FC<NodeProps<T>> {
|
||||
return function NodeAdapter(props) {
|
||||
return <SolarSystemNode {...props} />;
|
||||
};
|
||||
}
|
||||
@@ -1,63 +1,32 @@
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { useMemo } from 'react';
|
||||
import { WindowManager } from '@/hooks/Mapper/components/ui-kit/WindowManager';
|
||||
import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/types.ts';
|
||||
import { CURRENT_WINDOWS_VERSION, DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
type WindowsLS = {
|
||||
windows: WindowProps[];
|
||||
version: number;
|
||||
};
|
||||
|
||||
const saveWindowsToLS = (toSaveItems: WindowProps[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const out = toSaveItems.map(({ content, ...rest }) => rest);
|
||||
localStorage.setItem(SESSION_KEY.windows, JSON.stringify({ version: CURRENT_WINDOWS_VERSION, windows: out }));
|
||||
};
|
||||
|
||||
const restoreWindowsFromLS = (): WindowProps[] => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const raw = localStorage.getItem(SESSION_KEY.windows);
|
||||
if (!raw) {
|
||||
console.warn('No windows found in local storage!!');
|
||||
return DEFAULT_WIDGETS;
|
||||
}
|
||||
|
||||
const { version, windows } = JSON.parse(raw) as WindowsLS;
|
||||
if (!version || CURRENT_WINDOWS_VERSION > version) {
|
||||
return DEFAULT_WIDGETS;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-debugger
|
||||
const out = (windows as Omit<WindowProps, 'content'>[])
|
||||
.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id))
|
||||
.map(x => {
|
||||
const content = DEFAULT_WIDGETS.find(def => def.id === x.id)?.content;
|
||||
return { ...x, content: content! };
|
||||
});
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
export const MapInterface = () => {
|
||||
const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
|
||||
const { windowsVisible } = useMapRootState();
|
||||
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
|
||||
const { windowsSettings, updateWidgetSettings } = useMapRootState();
|
||||
|
||||
const itemsFiltered = useMemo(() => {
|
||||
return items.filter(x => windowsVisible.some(j => x.id === j));
|
||||
}, [items, windowsVisible]);
|
||||
const items = useMemo(() => {
|
||||
return windowsSettings.windows
|
||||
.map(x => {
|
||||
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;
|
||||
return {
|
||||
...x,
|
||||
content: content!,
|
||||
};
|
||||
})
|
||||
.filter(x => windowsSettings.visible.some(j => x.id === j));
|
||||
}, [windowsSettings]);
|
||||
|
||||
return (
|
||||
<WindowManager
|
||||
windows={itemsFiltered}
|
||||
windows={items}
|
||||
viewPort={windowsSettings.viewPort}
|
||||
dragSelector=".react-grid-dragHandleExample"
|
||||
onChange={x => {
|
||||
saveWindowsToLS(x);
|
||||
setItems(x);
|
||||
}}
|
||||
onChange={updateWidgetSettings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
@@ -58,7 +58,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
<Dialog
|
||||
header="Select signature to link"
|
||||
visible
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '500px' }}
|
||||
onHide={handleHide}
|
||||
contentClassName="!p-0"
|
||||
|
||||
@@ -4,17 +4,29 @@ import {
|
||||
RoutesWidget,
|
||||
SystemInfo,
|
||||
SystemSignatures,
|
||||
SystemStructures,
|
||||
SystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
export const CURRENT_WINDOWS_VERSION = 2;
|
||||
export const CURRENT_WINDOWS_VERSION = 8;
|
||||
export const WINDOWS_LOCAL_STORE_KEY = 'windows:settings:v2';
|
||||
|
||||
export enum WidgetsIds {
|
||||
info = 'info',
|
||||
signatures = 'signatures',
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
structures = 'structures',
|
||||
kills = 'kills',
|
||||
}
|
||||
|
||||
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
|
||||
WidgetsIds.info,
|
||||
WidgetsIds.local,
|
||||
WidgetsIds.routes,
|
||||
WidgetsIds.signatures,
|
||||
];
|
||||
|
||||
export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
{
|
||||
id: WidgetsIds.info,
|
||||
@@ -44,6 +56,20 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
zIndex: 0,
|
||||
content: () => <RoutesWidget />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.structures,
|
||||
position: { x: 10, y: 730 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemStructures />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.kills,
|
||||
position: { x: 270, y: 730 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <SystemKills />,
|
||||
},
|
||||
];
|
||||
|
||||
type WidgetsCheckboxesType = {
|
||||
@@ -68,4 +94,22 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
id: WidgetsIds.routes,
|
||||
label: 'Routes',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.structures,
|
||||
label: 'Structures',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.kills,
|
||||
label: 'Kills',
|
||||
},
|
||||
];
|
||||
|
||||
export function getWidgetsCheckboxesProps(detailedKillsDisabled: boolean): WidgetsCheckboxesType {
|
||||
return filterOutKills(WIDGETS_CHECKBOXES_PROPS, detailedKillsDisabled);
|
||||
}
|
||||
|
||||
|
||||
function filterOutKills<T extends { id: WidgetsIds }>(items: T[], shouldFilter: boolean) {
|
||||
if (!shouldFilter) return items;
|
||||
return items.filter((w) => w.id !== WidgetsIds.kills);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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';
|
||||
|
||||
export const SystemKills: React.FC = () => {
|
||||
const {
|
||||
data: { selectedSystems, systems },
|
||||
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;
|
||||
|
||||
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)} />}>
|
||||
{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 && (!kills || kills.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={kills}
|
||||
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,180 @@
|
||||
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_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,
|
||||
|
||||
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;
|
||||
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);
|
||||
|
||||
const { victimCorpLogoUrl, victimAllianceLogoUrl } = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
const { attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
const { url: victimPrimaryLogoUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim',
|
||||
);
|
||||
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id || 0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'h-10 flex items-center border-b border-stone-800',
|
||||
'text-xs whitespace-nowrap overflow-hidden leading-none',
|
||||
)}
|
||||
>
|
||||
{victimPrimaryLogoUrl && (
|
||||
<WdTooltipWrapper content={victimPrimaryTooltip} position={TooltipPosition.top}>
|
||||
<a
|
||||
href={zkillLink('kill', killmail_id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative 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 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 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]',
|
||||
)}
|
||||
style={{ bottom: 0, right: 0 }}
|
||||
>
|
||||
{attackerSubscript.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
// FullKillRow.tsx
|
||||
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_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 = '',
|
||||
|
||||
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;
|
||||
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';
|
||||
|
||||
const { victimPortraitUrl, victimCorpLogoUrl, victimAllianceLogoUrl } = buildVictimImageUrls({
|
||||
victim_char_id,
|
||||
victim_ship_type_id,
|
||||
victim_corp_id,
|
||||
victim_alliance_id,
|
||||
});
|
||||
const { attackerPortraitUrl, attackerCorpLogoUrl, attackerAllianceLogoUrl } = buildAttackerImageUrls({
|
||||
final_blow_char_id,
|
||||
final_blow_corp_id,
|
||||
final_blow_alliance_id,
|
||||
});
|
||||
|
||||
const { url: victimPrimaryImageUrl, tooltip: victimPrimaryTooltip } = getPrimaryLogoAndTooltip(
|
||||
victimAllianceLogoUrl,
|
||||
victimCorpLogoUrl,
|
||||
victim_alliance_name,
|
||||
victim_corp_name,
|
||||
'Victim',
|
||||
);
|
||||
|
||||
const { url: attackerPrimaryImageUrl, tooltip: attackerPrimaryTooltip } = getAttackerPrimaryImageAndTooltip(
|
||||
attackerIsNpc,
|
||||
attackerAllianceLogoUrl,
|
||||
attackerCorpLogoUrl,
|
||||
final_blow_alliance_name,
|
||||
final_blow_corp_name,
|
||||
final_blow_ship_type_id || 0,
|
||||
);
|
||||
|
||||
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">
|
||||
{/* Victim top-level logo (corp or alliance), with tooltip */}
|
||||
{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 !== null && 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,136 @@
|
||||
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,
|
||||
excludedSystems: globalSettings.excludedSystems || [],
|
||||
});
|
||||
|
||||
const [, forceRender] = useState(0);
|
||||
|
||||
const [addSystemDialogVisible, setAddSystemDialogVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
localRef.current = {
|
||||
compact: globalSettings.compact,
|
||||
showAll: globalSettings.showAll,
|
||||
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 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 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>
|
||||
|
||||
{/* Apply + Close button row */}
|
||||
<div className="flex gap-2 justify-end mt-4">
|
||||
<Button onClick={handleApply} label="Apply" outlined size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AddSystemDialog for picking new systems to exclude */}
|
||||
<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,
|
||||
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,51 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
export interface KillsWidgetSettings {
|
||||
compact: boolean;
|
||||
showAll: boolean;
|
||||
excludedSystems: number[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_KILLS_WIDGET_SETTINGS: KillsWidgetSettings = {
|
||||
compact: false,
|
||||
showAll: false,
|
||||
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,178 @@
|
||||
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;
|
||||
|
||||
const visibleSystemIds = useMemo(() => {
|
||||
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||
}, [systems, excludedSystems]);
|
||||
|
||||
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: visibleSystemIds,
|
||||
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 => `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, visibleSystemIds, sinceHours, mergeKillsIntoGlobal],
|
||||
);
|
||||
|
||||
const debouncedFetchKills = useMemo(
|
||||
() =>
|
||||
debounce(fetchKills, 500, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
}),
|
||||
[fetchKills],
|
||||
);
|
||||
|
||||
const finalKills = useMemo(() => {
|
||||
if (showAllVisible) {
|
||||
return visibleSystemIds.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 visibleSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||
}
|
||||
return [];
|
||||
}, [showAllVisible, systemId, didFallbackFetch, visibleSystemIds, 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 showAll
|
||||
}
|
||||
}, [systemId, showAllVisible, debouncedFetchKills, fetchKills, didFallbackFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibleSystemIds.length === 0) return;
|
||||
|
||||
if (showAllVisible || systemId) {
|
||||
debouncedFetchKills();
|
||||
// Clean up the debounce on unmount or changes
|
||||
return () => debouncedFetchKills.cancel();
|
||||
}
|
||||
}, [showAllVisible, systemId, visibleSystemIds, 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 && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useCallback, ClipboardEvent, useRef } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import {
|
||||
LayoutEventBlocker,
|
||||
WdImgButton,
|
||||
TooltipPosition,
|
||||
InfoDrawer,
|
||||
SystemView,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
|
||||
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
|
||||
import { useSystemStructures } from './hooks/useSystemStructures';
|
||||
import { processSnippetText } from './helpers';
|
||||
|
||||
export const SystemStructures: React.FC = () => {
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
const [systemId] = selectedSystems;
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
|
||||
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(labelRef, 260);
|
||||
|
||||
const processClipboard = useCallback(
|
||||
(text: string) => {
|
||||
const updated = processSnippetText(text, structures);
|
||||
handleUpdateStructures(updated);
|
||||
},
|
||||
[structures, handleUpdateStructures],
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(e: ClipboardEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
processClipboard(e.clipboardData.getData('text'));
|
||||
},
|
||||
[processClipboard],
|
||||
);
|
||||
|
||||
const handlePasteTimer = useCallback(async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
processClipboard(text);
|
||||
} catch (err) {
|
||||
console.error('Clipboard read error:', err);
|
||||
}
|
||||
}, [processClipboard]);
|
||||
|
||||
function renderWidgetLabel() {
|
||||
return (
|
||||
<div className="flex justify-between items-center text-xs w-full h-full" ref={labelRef}>
|
||||
<div className="flex justify-between items-center gap-1">
|
||||
{!isCompact && (
|
||||
<div className="flex whitespace-nowrap text-ellipsis overflow-hidden text-stone-400">
|
||||
Structures
|
||||
{!isNotSelectedSystem && ' in'}
|
||||
</div>
|
||||
)}
|
||||
{!isNotSelectedSystem && <SystemView systemId={systemId} className="select-none text-center" hideRegion />}
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2.5">
|
||||
<WdImgButton
|
||||
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
|
||||
onClick={handlePasteTimer}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: 'Add Structures/Timer',
|
||||
}}
|
||||
/>
|
||||
<WdImgButton
|
||||
className={PrimeIcons.QUESTION_CIRCLE}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: (
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add/update structures?</b>}>
|
||||
In game, select one or more structures in D-Scan and then
|
||||
<br />
|
||||
use the blue add structure data button
|
||||
</InfoDrawer>
|
||||
<InfoDrawer title={<b className="text-slate-50">How to add a timer?</b>}>
|
||||
In game, select a structure with an active timer, right click to copy, and then
|
||||
<span className="text-blue-500"> blue </span>
|
||||
use the blue add structure data button
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</LayoutEventBlocker>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div tabIndex={0} onPaste={handlePaste} className="h-full flex flex-col" style={{ outline: 'none' }}>
|
||||
<Widget label={renderWidgetLabel()}>
|
||||
{isNotSelectedSystem ? (
|
||||
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
|
||||
System is not selected
|
||||
</div>
|
||||
) : (
|
||||
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
|
||||
)}
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
.TableRowCompact {
|
||||
height: 8px;
|
||||
max-height: 8px;
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Table .p-datatable-tbody > tr > td {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Tooltip {
|
||||
white-space: pre-line;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { DataTable, DataTableRowClickEvent } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SystemStructuresDialog } from '../SystemStructuresDialog/SystemStructuresDialog';
|
||||
import { StructureItem } from '../helpers/structureTypes';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import classes from './SystemStructuresContent.module.scss';
|
||||
import { renderOwnerCell, renderTypeCell, renderTimerCell } from '../renders/cellRenders';
|
||||
|
||||
interface SystemStructuresContentProps {
|
||||
structures: StructureItem[];
|
||||
onUpdateStructures: (newList: StructureItem[]) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresContent: React.FC<SystemStructuresContentProps> = ({ structures, onUpdateStructures }) => {
|
||||
const [selectedRow, setSelectedRow] = useState<StructureItem | null>(null);
|
||||
const [editingItem, setEditingItem] = useState<StructureItem | null>(null);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const handleRowClick = (e: DataTableRowClickEvent) => {
|
||||
const row = e.data as StructureItem;
|
||||
setSelectedRow(prev => (prev?.id === row.id ? null : row));
|
||||
};
|
||||
|
||||
const handleRowDoubleClick = (e: DataTableRowClickEvent) => {
|
||||
setEditingItem(e.data as StructureItem);
|
||||
setShowEditDialog(true);
|
||||
};
|
||||
|
||||
// Press Delete => remove selected row
|
||||
const handleDeleteSelected = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!selectedRow) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const newList = structures.filter(s => s.id !== selectedRow.id);
|
||||
onUpdateStructures(newList);
|
||||
setSelectedRow(null);
|
||||
},
|
||||
[selectedRow, structures, onUpdateStructures],
|
||||
);
|
||||
|
||||
useHotkey(false, ['Delete', 'Backspace'], handleDeleteSelected);
|
||||
|
||||
const visibleStructures = useMemo(() => {
|
||||
return structures;
|
||||
}, [structures]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2 text-xs text-stone-200 h-full">
|
||||
{visibleStructures.length === 0 ? (
|
||||
<div className="flex-1 flex justify-center items-center text-stone-400/80 text-sm">No structures</div>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
<DataTable
|
||||
value={visibleStructures}
|
||||
dataKey="id"
|
||||
className={clsx(classes.Table, 'w-full select-none h-full')}
|
||||
size="small"
|
||||
sortMode="single"
|
||||
rowHover
|
||||
style={{ tableLayout: 'fixed', width: '100%' }}
|
||||
onRowClick={handleRowClick}
|
||||
onRowDoubleClick={handleRowDoubleClick}
|
||||
rowClassName={rowData => {
|
||||
const isSelected = selectedRow?.id === rowData.id;
|
||||
return clsx(
|
||||
classes.TableRowCompact,
|
||||
'transition-colors duration-200 cursor-pointer',
|
||||
isSelected ? 'bg-amber-500/50 hover:bg-amber-500/70' : 'hover:bg-purple-400/20',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Column
|
||||
header="Type"
|
||||
body={renderTypeCell}
|
||||
style={{
|
||||
width: '160px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
field="name"
|
||||
header="Name"
|
||||
style={{
|
||||
width: '120px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
header="Owner"
|
||||
body={renderOwnerCell}
|
||||
style={{
|
||||
width: '120px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
field="status"
|
||||
header="Status"
|
||||
style={{
|
||||
width: '100px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
header="Timer"
|
||||
body={renderTimerCell}
|
||||
style={{
|
||||
width: '110px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
body={(rowData: StructureItem) => (
|
||||
<i
|
||||
className={clsx(PrimeIcons.PENCIL, 'text-[14px] cursor-pointer')}
|
||||
title="Edit"
|
||||
onClick={() => {
|
||||
setEditingItem(rowData);
|
||||
setShowEditDialog(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{
|
||||
width: '40px',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEditDialog && editingItem && (
|
||||
<SystemStructuresDialog
|
||||
visible={showEditDialog}
|
||||
structure={editingItem}
|
||||
onClose={() => setShowEditDialog(false)}
|
||||
onSave={(updatedItem: StructureItem) => {
|
||||
const newList = structures.map(s => (s.id === updatedItem.id ? updatedItem : s));
|
||||
onUpdateStructures(newList);
|
||||
setShowEditDialog(false);
|
||||
}}
|
||||
onDelete={(deleteId: string) => {
|
||||
const newList = structures.filter(s => s.id !== deleteId);
|
||||
onUpdateStructures(newList);
|
||||
setShowEditDialog(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
.systemStructureDialog {
|
||||
|
||||
.p-dialog-content {
|
||||
background-color: var(--surface-800) !important;
|
||||
}
|
||||
|
||||
.p-dialog-header {
|
||||
background-color: var(--surface-700);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icon,
|
||||
.p-dialog-header-title {
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.p-inputtext {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ddd !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
.p-button {
|
||||
font-size: 12px !important;
|
||||
padding: 0.3rem 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { StructureItem, StructureStatus, statusesRequiringTimer, formatToISO } from '../helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
|
||||
interface StructuresEditDialogProps {
|
||||
visible: boolean;
|
||||
structure?: StructureItem;
|
||||
onClose: () => void;
|
||||
onSave: (updatedItem: StructureItem) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
visible,
|
||||
structure,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
}) => {
|
||||
const [editData, setEditData] = useState<StructureItem | null>(null);
|
||||
const [ownerInput, setOwnerInput] = useState('');
|
||||
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (structure) {
|
||||
setEditData(structure);
|
||||
setOwnerInput(structure.ownerName ?? '');
|
||||
} else {
|
||||
setEditData(null);
|
||||
setOwnerInput('');
|
||||
}
|
||||
}, [structure]);
|
||||
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item =>
|
||||
item.label.toLowerCase().includes(newQuery.toLowerCase()),
|
||||
);
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch owners:', err);
|
||||
setOwnerSuggestions([]);
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
);
|
||||
|
||||
const handleChange = (field: keyof StructureItem, val: string | Date) => {
|
||||
// If we want to forbid changing structureTypeId or structureType from the dialog, do so here:
|
||||
if (field === 'structureTypeId' || field === 'structureType') return;
|
||||
|
||||
setEditData(prev => {
|
||||
if (!prev) return null;
|
||||
|
||||
// If this is the endTime (Date from Calendar), we store as ISO or string:
|
||||
if (field === 'endTime' && val instanceof Date) {
|
||||
return { ...prev, endTime: val.toISOString() };
|
||||
}
|
||||
|
||||
return { ...prev, [field]: val };
|
||||
});
|
||||
};
|
||||
|
||||
// when user picks a corp from auto-complete
|
||||
const handleSelectOwner = (selected: { label: string; value: string }) => {
|
||||
setOwnerInput(selected.label);
|
||||
setEditData(prev =>
|
||||
prev ? { ...prev, ownerName: selected.label, ownerId: selected.value } : null,
|
||||
);
|
||||
};
|
||||
|
||||
const handleStatusChange = (val: string) => {
|
||||
setEditData(prev => {
|
||||
if (!prev) return null;
|
||||
const newStatus = val as StructureStatus;
|
||||
// If new status doesn't require a timer, we clear out endTime
|
||||
const newEndTime = statusesRequiringTimer.includes(newStatus) ? prev.endTime : '';
|
||||
return { ...prev, status: newStatus, endTime: newEndTime };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
if (!editData) return;
|
||||
|
||||
// If status doesn't require a timer, clear endTime
|
||||
if (!statusesRequiringTimer.includes(editData.status)) {
|
||||
editData.endTime = '';
|
||||
} else if (editData.endTime) {
|
||||
// convert to full ISO if not already
|
||||
editData.endTime = formatToISO(editData.endTime);
|
||||
}
|
||||
|
||||
// fetch corporation ticker if we have an ownerId
|
||||
if (editData.ownerId) {
|
||||
try {
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: editData.ownerId },
|
||||
});
|
||||
editData.ownerTicker = ticker ?? '';
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ticker:', err);
|
||||
editData.ownerTicker = '';
|
||||
}
|
||||
}
|
||||
|
||||
onSave(editData);
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
if (!editData) return;
|
||||
onDelete(editData.id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!editData) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onHide={onClose}
|
||||
header={`Edit Structure - ${editData.name ?? ''}`}
|
||||
className={clsx('myStructuresDialog', 'text-stone-200 w-full max-w-md')}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-[14px]">
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Type:</span>
|
||||
<input
|
||||
readOnly
|
||||
className="p-inputtext p-component cursor-not-allowed"
|
||||
value={editData.structureType ?? ''}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Name:</span>
|
||||
<input
|
||||
className="p-inputtext p-component"
|
||||
value={editData.name ?? ''}
|
||||
onChange={e => handleChange('name', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Owner:</span>
|
||||
<AutoComplete
|
||||
id="owner"
|
||||
value={ownerInput}
|
||||
suggestions={ownerSuggestions}
|
||||
completeMethod={searchOwners}
|
||||
minLength={3}
|
||||
delay={400}
|
||||
field="label"
|
||||
placeholder="Corporation name..."
|
||||
onChange={e => setOwnerInput(e.value)}
|
||||
onSelect={e => handleSelectOwner(e.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Status:</span>
|
||||
<select
|
||||
className="p-inputtext p-component"
|
||||
value={editData.status}
|
||||
onChange={e => handleStatusChange(e.target.value)}
|
||||
>
|
||||
<option value="Powered">Powered</option>
|
||||
<option value="Anchoring">Anchoring</option>
|
||||
<option value="Unanchoring">Unanchoring</option>
|
||||
<option value="Low Power">Low Power</option>
|
||||
<option value="Abandoned">Abandoned</option>
|
||||
<option value="Reinforced">Reinforced</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{statusesRequiringTimer.includes(editData.status) && (
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Timer <br /> (Eve Time):</span>
|
||||
<Calendar
|
||||
value={editData.endTime ? new Date(editData.endTime) : undefined}
|
||||
onChange={(e) => handleChange('endTime', e.value ?? '')}
|
||||
showTime
|
||||
hourFormat="24"
|
||||
dateFormat="yy-mm-dd"
|
||||
showIcon
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
|
||||
<span className="mt-1">Notes:</span>
|
||||
<textarea
|
||||
className="p-inputtext p-component resize-none h-24"
|
||||
value={editData.notes ?? ''}
|
||||
onChange={e => handleChange('notes', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 mt-4">
|
||||
<Button label="Delete" severity="danger" className="p-button-sm" onClick={handleDeleteClick} />
|
||||
<Button label="Save" className="p-button-sm" onClick={handleSaveClick} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './parserHelper';
|
||||
export * from './pasteParser';
|
||||
export * from './structureTypes';
|
||||
export * from './structureUtils';
|
||||
@@ -0,0 +1,92 @@
|
||||
import { StructureStatus, StructureItem, STRUCTURE_TYPE_MAP } from './structureTypes';
|
||||
import { formatToISO } from './structureUtils';
|
||||
|
||||
// Up to you if you'd like to keep a separate constant here or not
|
||||
export const statusesRequiringTimer: StructureStatus[] = ['Anchoring', 'Reinforced'];
|
||||
|
||||
/**
|
||||
* parseFormatOneLine(line):
|
||||
* - Splits by tabs
|
||||
* - First col => structureTypeId
|
||||
* - Second col => rawName
|
||||
* - Third col => structureTypeName
|
||||
*/
|
||||
export function parseFormatOneLine(line: string): StructureItem | null {
|
||||
const columns = line
|
||||
.split('\t')
|
||||
.map(c => c.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// Expecting e.g. "35832 J214811 - SomeName Astrahus"
|
||||
if (columns.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [rawTypeId, rawName, rawTypeName] = columns;
|
||||
|
||||
if (columns.length != 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!STRUCTURE_TYPE_MAP[rawTypeId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawTypeName != STRUCTURE_TYPE_MAP[rawTypeId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = rawName.replace(/^J\d{6}\s*-\s*/, '').trim();
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
structureTypeId: rawTypeId,
|
||||
structureType: rawTypeName,
|
||||
name,
|
||||
ownerName: '',
|
||||
notes: '',
|
||||
status: 'Powered', // Default
|
||||
endTime: '', // No timer by default
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesThreeLineSnippet(lines: string[]): boolean {
|
||||
if (lines.length < 3) return false;
|
||||
return /until\s+\d{4}\.\d{2}\.\d{2}/i.test(lines[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseThreeLineSnippet:
|
||||
* - Example lines:
|
||||
* line1: "J214811 - Folgers"
|
||||
* line2: "1,475 km"
|
||||
* line3: "Reinforced until 2025.01.13 23:51"
|
||||
*/
|
||||
export function parseThreeLineSnippet(lines: string[]): StructureItem {
|
||||
const [line1, , line3] = lines;
|
||||
|
||||
let status: StructureStatus = 'Reinforced';
|
||||
let endTime: string | undefined;
|
||||
|
||||
// e.g. "Reinforced until 2025.01.13 23:27"
|
||||
const match = line3.match(/^(?<stat>\w+)\s+until\s+(?<dateTime>[\d.]+\s+[\d:]+)/i);
|
||||
|
||||
if (match?.groups?.stat) {
|
||||
const candidateStatus = match.groups.stat as StructureStatus;
|
||||
if (statusesRequiringTimer.includes(candidateStatus)) {
|
||||
status = candidateStatus;
|
||||
}
|
||||
}
|
||||
if (match?.groups?.dateTime) {
|
||||
let dt = match.groups.dateTime.trim().replace(/\./g, '-'); // "2025-01-13 23:27"
|
||||
dt = dt.replace(' ', 'T'); // "2025-01-13T23:27"
|
||||
endTime = formatToISO(dt); // => "2025-01-13T23:27:00Z"
|
||||
}
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
name: line1.replace(/^J\d{6}\s*-\s*/, '').trim(),
|
||||
status,
|
||||
endTime,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { StructureItem } from './structureTypes';
|
||||
import { parseThreeLineSnippet, parseFormatOneLine, matchesThreeLineSnippet } from './parserHelper';
|
||||
|
||||
export function processSnippetText(rawText: string, existingStructures: StructureItem[]): StructureItem[] {
|
||||
if (!rawText) {
|
||||
return existingStructures.slice();
|
||||
}
|
||||
|
||||
const lines = rawText
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (lines.length === 3 && matchesThreeLineSnippet(lines)) {
|
||||
return applyThreeLineSnippet(lines, existingStructures);
|
||||
} else {
|
||||
return applySingleLineParse(lines, existingStructures);
|
||||
}
|
||||
}
|
||||
|
||||
function applyThreeLineSnippet(snippetLines: string[], existingStructures: StructureItem[]): StructureItem[] {
|
||||
const updatedList = [...existingStructures];
|
||||
const snippetItem = parseThreeLineSnippet(snippetLines);
|
||||
|
||||
const existingIndex = updatedList.findIndex(s => s.name.trim() === snippetItem.name.trim());
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existing = updatedList[existingIndex];
|
||||
updatedList[existingIndex] = {
|
||||
...existing,
|
||||
status: snippetItem.status,
|
||||
endTime: snippetItem.endTime,
|
||||
};
|
||||
}
|
||||
|
||||
return updatedList;
|
||||
}
|
||||
|
||||
function applySingleLineParse(lines: string[], existingStructures: StructureItem[]): StructureItem[] {
|
||||
const updatedList = [...existingStructures];
|
||||
const newItems: StructureItem[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const item = parseFormatOneLine(line);
|
||||
if (!item) continue;
|
||||
|
||||
const isDuplicate = updatedList.some(
|
||||
s => s.structureTypeId === item.structureTypeId && s.name.trim() === item.name.trim(),
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [...updatedList, ...newItems];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export type StructureStatus = 'Powered' | 'Anchoring' | 'Unanchoring' | 'Low Power' | 'Abandoned' | 'Reinforced';
|
||||
|
||||
export interface StructureItem {
|
||||
id: string;
|
||||
systemId?: string;
|
||||
structureTypeId?: string;
|
||||
structureType?: string;
|
||||
name: string;
|
||||
ownerName?: string;
|
||||
ownerId?: string;
|
||||
ownerTicker?: string;
|
||||
notes?: string;
|
||||
status: StructureStatus;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
export const STRUCTURE_TYPE_MAP: Record<string, string> = {
|
||||
'35825': 'Raitaru',
|
||||
'35826': 'Azbel',
|
||||
'35827': 'Sotiyo',
|
||||
'35832': 'Astrahus',
|
||||
'35833': 'Fortizar',
|
||||
'35834': 'Keepstar',
|
||||
'35835': 'Athanor',
|
||||
'35836': 'Tatara',
|
||||
'40340': 'Upwell Palatine Keepstar',
|
||||
'47512': "'Moreau' Fortizar",
|
||||
'47513': "'Draccous' Fortizar",
|
||||
'47514': "'Horizon' Fortizar",
|
||||
'47515': "'Marginis' Fortizar",
|
||||
'47516': "'Prometheus' Fortizar",
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { StructureItem } from './structureTypes';
|
||||
|
||||
export function getActualStructures(oldList: StructureItem[], newList: StructureItem[]) {
|
||||
const oldMap = new Map(oldList.map(s => [s.id, s]));
|
||||
const newMap = new Map(newList.map(s => [s.id, s]));
|
||||
|
||||
const added: StructureItem[] = [];
|
||||
const updated: StructureItem[] = [];
|
||||
const removed: StructureItem[] = [];
|
||||
|
||||
for (const newItem of newList) {
|
||||
const oldItem = oldMap.get(newItem.id);
|
||||
if (!oldItem) {
|
||||
added.push(newItem);
|
||||
} else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
|
||||
updated.push(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldItem of oldList) {
|
||||
if (!newMap.has(oldItem.id)) {
|
||||
removed.push(oldItem);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, updated, removed };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function mapServerStructure(serverData: any): StructureItem {
|
||||
const { owner_id, owner_ticker, structure_type_id, structure_type, owner_name, end_time, system_id, ...rest } =
|
||||
serverData;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
ownerId: owner_id,
|
||||
ownerTicker: owner_ticker,
|
||||
ownerName: owner_name,
|
||||
structureType: structure_type,
|
||||
structureTypeId: structure_type_id,
|
||||
endTime: end_time ?? '',
|
||||
systemId: system_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatToISO(datetimeLocal: string): string {
|
||||
if (!datetimeLocal) return '';
|
||||
|
||||
// If missing seconds, add :00
|
||||
let iso = datetimeLocal;
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(iso)) {
|
||||
iso += ':00';
|
||||
}
|
||||
// Ensure trailing 'Z'
|
||||
if (!iso.endsWith('Z')) {
|
||||
iso += 'Z';
|
||||
}
|
||||
return iso;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { mapServerStructure, getActualStructures, StructureItem, statusesRequiringTimer } from '../helpers';
|
||||
|
||||
interface UseSystemStructuresProps {
|
||||
systemId: string | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
outCommand: (payload: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export function useSystemStructures({ systemId, outCommand }: UseSystemStructuresProps) {
|
||||
const [structures, setStructures] = useState<StructureItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchStructures = useCallback(async () => {
|
||||
if (!systemId) {
|
||||
setStructures([]);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const { structures: fetched = [] } = await outCommand({
|
||||
type: OutCommand.getStructures,
|
||||
data: { system_id: systemId },
|
||||
});
|
||||
|
||||
const mappedStructures = fetched.map(mapServerStructure);
|
||||
setStructures(mappedStructures);
|
||||
} catch (err) {
|
||||
console.error('Failed to get structures:', err);
|
||||
setError('Error fetching structures');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [systemId, outCommand]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStructures();
|
||||
}, [fetchStructures]);
|
||||
|
||||
const sanitizeEndTimers = useCallback((item: StructureItem) => {
|
||||
if (!statusesRequiringTimer.includes(item.status)) {
|
||||
item.endTime = '';
|
||||
}
|
||||
return item;
|
||||
}, []);
|
||||
|
||||
const sanitizeIds = useCallback((item: StructureItem) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { id, ...rest } = item;
|
||||
return rest;
|
||||
}, []);
|
||||
|
||||
const handleUpdateStructures = useCallback(
|
||||
async (newList: StructureItem[]) => {
|
||||
const { added, updated, removed } = getActualStructures(structures, newList);
|
||||
|
||||
const sanitizedAdded = added.map(sanitizeIds);
|
||||
const sanitizedUpdated = updated.map(sanitizeEndTimers);
|
||||
|
||||
try {
|
||||
const { structures: updatedStructures = [] } = await outCommand({
|
||||
type: OutCommand.updateStructures,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
added: sanitizedAdded,
|
||||
updated: sanitizedUpdated,
|
||||
removed,
|
||||
},
|
||||
});
|
||||
|
||||
const finalStructures = updatedStructures.map(mapServerStructure);
|
||||
setStructures(finalStructures);
|
||||
} catch (err) {
|
||||
console.error('Failed to update structures:', err);
|
||||
}
|
||||
},
|
||||
[structures, systemId, outCommand, sanitizeIds, sanitizeEndTimers],
|
||||
);
|
||||
|
||||
return { structures, handleUpdateStructures, isLoading, error };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SystemStructures';
|
||||
@@ -0,0 +1,50 @@
|
||||
// File: TimerCell.tsx
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { StructureStatus } from '../helpers/structureTypes';
|
||||
import { statusesRequiringTimer } from '../helpers';
|
||||
|
||||
interface TimerCellProps {
|
||||
endTime?: string;
|
||||
status: StructureStatus;
|
||||
}
|
||||
|
||||
function TimerCellImpl({ endTime, status }: TimerCellProps) {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (!endTime || !statusesRequiringTimer.includes(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setNow(Date.now());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [endTime, status]);
|
||||
|
||||
if (!statusesRequiringTimer.includes(status)) {
|
||||
return <span className="text-stone-400"></span>;
|
||||
}
|
||||
if (!endTime) {
|
||||
return <span className="text-sky-400">Set Timer</span>;
|
||||
}
|
||||
|
||||
const msLeft = new Date(endTime).getTime() - now;
|
||||
if (msLeft <= 0) {
|
||||
return <span className="text-red-500">00:00:00</span>;
|
||||
}
|
||||
|
||||
const sec = Math.floor(msLeft / 1000) % 60;
|
||||
const min = Math.floor(msLeft / (1000 * 60)) % 60;
|
||||
const hr = Math.floor(msLeft / (1000 * 3600));
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
return (
|
||||
<span className="text-sky-400">
|
||||
{pad(hr)}:{pad(min)}:{pad(sec)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export const TimerCell = React.memo(TimerCellImpl);
|
||||
@@ -0,0 +1,36 @@
|
||||
import { StructureItem } from '../helpers';
|
||||
import { TimerCell } from './TimerCell';
|
||||
|
||||
export function renderTimerCell(row: StructureItem) {
|
||||
return <TimerCell endTime={row.endTime} status={row.status} />;
|
||||
}
|
||||
|
||||
export function renderOwnerCell(row: StructureItem) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{row.ownerId && (
|
||||
<img
|
||||
src={`https://images.evetech.net/corporations/${row.ownerId}/logo?size=32`}
|
||||
alt="corp icon"
|
||||
className="w-5 h-5 object-contain"
|
||||
/>
|
||||
)}
|
||||
<span>{row.ownerTicker || row.ownerName}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderTypeCell(row: StructureItem) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{row.structureTypeId && (
|
||||
<img
|
||||
src={`https://images.evetech.net/types/${row.structureTypeId}/icon`}
|
||||
alt="icon"
|
||||
className="w-5 h-5 object-contain"
|
||||
/>
|
||||
)}
|
||||
<span>{row.structureType ?? ''}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,3 +2,5 @@ export * from './LocalCharacters';
|
||||
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>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
.p-tabview-panels {
|
||||
padding: 6px 1rem !important;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.p-tabview-nav-container {
|
||||
|
||||
@@ -221,11 +221,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className={styles.verticalTabsContainer}>
|
||||
<TabView
|
||||
activeIndex={activeIndex}
|
||||
onTabChange={e => setActiveIndex(e.index)}
|
||||
className={styles.verticalTabView}
|
||||
>
|
||||
<TabView activeIndex={activeIndex} onTabChange={e => setActiveIndex(e.index)}>
|
||||
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
|
||||
<div className="w-full h-full flex flex-col gap-1">{renderSettingsList(COMMON_CHECKBOXES_PROPS)}</div>
|
||||
</TabPanel>
|
||||
@@ -246,7 +242,7 @@ export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
|
||||
{renderSettingsList(UI_CHECKBOXES_PROPS)}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Widgets" headerClassName={styles.verticalTabHeader}>
|
||||
<TabPanel header="Widgets" className="h-full" headerClassName={styles.verticalTabHeader}>
|
||||
<WidgetsSettings />
|
||||
</TabPanel>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styles from './MapSettings.module.scss';
|
||||
|
||||
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
interface PrettySwitchboxProps {
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
import { PrettySwitchbox } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components';
|
||||
import { WIDGETS_CHECKBOXES_PROPS, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { getWidgetsCheckboxesProps, WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Button } from 'primereact/button';
|
||||
|
||||
export interface WidgetsSettingsProps {}
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
|
||||
const { windowsVisible, setWindowsVisible } = useMapRootState();
|
||||
const { windowsSettings, toggleWidgetVisibility, resetWidgets, data } = useMapRootState();
|
||||
|
||||
const handleWidgetSettingsChange = useCallback(
|
||||
(widget: WidgetsIds, checked: boolean) => {
|
||||
setWindowsVisible(prev => {
|
||||
if (checked) {
|
||||
return [...prev, widget];
|
||||
}
|
||||
|
||||
return prev.filter(x => x !== widget);
|
||||
});
|
||||
},
|
||||
[setWindowsVisible],
|
||||
(widget: WidgetsIds) => toggleWidgetVisibility(widget),
|
||||
[toggleWidgetVisibility],
|
||||
);
|
||||
|
||||
const detailedKillsDisabled = data.options?.detailedKillsDisabled === true;
|
||||
const widgetProps = getWidgetsCheckboxesProps(detailedKillsDisabled);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{WIDGETS_CHECKBOXES_PROPS.map(widget => (
|
||||
<PrettySwitchbox
|
||||
key={widget.id}
|
||||
label={widget.label}
|
||||
checked={windowsVisible.some(x => x === widget.id)}
|
||||
setChecked={checked => handleWidgetSettingsChange(widget.id, checked)}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-col h-full gap-2">
|
||||
<div>
|
||||
{widgetProps.map(widget => (
|
||||
<PrettySwitchbox
|
||||
key={widget.id}
|
||||
label={widget.label}
|
||||
checked={windowsSettings.visible.some(x => x === widget.id)}
|
||||
setChecked={() => handleWidgetSettingsChange(widget.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto]">
|
||||
<div />
|
||||
<Button className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets"></Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -14,9 +14,10 @@ import classes from './MapWrapper.module.scss';
|
||||
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
|
||||
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { Node, XYPosition } from 'reactflow';
|
||||
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
|
||||
|
||||
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
|
||||
@@ -32,7 +33,7 @@ export const MapWrapper = () => {
|
||||
const {
|
||||
update,
|
||||
outCommand,
|
||||
data: { selectedConnections, selectedSystems, hubs, systems },
|
||||
data: { selectedConnections, selectedSystems, hubs, systems, connections, linkSignatureToSystem },
|
||||
interfaceSettings: {
|
||||
isShowMenu,
|
||||
isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap,
|
||||
@@ -46,25 +47,19 @@ export const MapWrapper = () => {
|
||||
const { deleteSystems } = useDeleteSystems();
|
||||
const { mapRef, runCommand } = useCommonMapEventProcessor();
|
||||
|
||||
const { updateLinkSignatureToSystem } = useCommandsSystems();
|
||||
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
|
||||
const { handleSystemMultipleContext, ...systemMultipleCtxProps } = useContextMenuSystemMultipleHandlers();
|
||||
|
||||
const [openSettings, setOpenSettings] = useState<string | null>(null);
|
||||
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
|
||||
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
|
||||
const [openAddSystem, setOpenAddSystem] = useState<XYPosition | null>(null);
|
||||
const [selectedConnection, setSelectedConnection] = useState<SolarSystemConnection | null>(null);
|
||||
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, deleteSystems };
|
||||
const ref = useRef({ selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems });
|
||||
ref.current = { selectedConnections, selectedSystems, systemContextProps, systems, connections, deleteSystems };
|
||||
|
||||
useMapEventListener(event => {
|
||||
switch (event.name) {
|
||||
case Commands.linkSignatureToSystem:
|
||||
setOpenLinkSignatures(event.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
runCommand(event);
|
||||
});
|
||||
|
||||
@@ -93,9 +88,6 @@ export const MapWrapper = () => {
|
||||
case OutCommand.openSettings:
|
||||
setOpenSettings(event.data.system_id);
|
||||
break;
|
||||
case OutCommand.linkSignatureToSystem:
|
||||
setOpenLinkSignatures(event.data);
|
||||
break;
|
||||
default:
|
||||
return outCommand(event);
|
||||
}
|
||||
@@ -133,6 +125,11 @@ export const MapWrapper = () => {
|
||||
setOpenAddSystem(coordinates);
|
||||
}, []);
|
||||
|
||||
const canRemoveConnection = useCallback((connectionId: string) => {
|
||||
const { connections } = ref.current;
|
||||
return !connections.some(x => x.id === connectionId);
|
||||
}, []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => {
|
||||
if (ref.current.systems.some(x => x.system_static_info.solar_system_id === item.value)) {
|
||||
@@ -169,6 +166,7 @@ export const MapWrapper = () => {
|
||||
isSoftBackground={isSoftBackground}
|
||||
theme={theme}
|
||||
onAddSystem={onAddSystem}
|
||||
canRemoveConnection={canRemoveConnection}
|
||||
/>
|
||||
|
||||
{openSettings != null && (
|
||||
@@ -179,8 +177,8 @@ export const MapWrapper = () => {
|
||||
<SystemCustomLabelDialog systemId={openCustomLabel} visible setVisible={() => setOpenCustomLabel(null)} />
|
||||
)}
|
||||
|
||||
{openLinkSignatures != null && (
|
||||
<SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} />
|
||||
{linkSignatureToSystem != null && (
|
||||
<SystemLinkSignatureDialog data={linkSignatureToSystem} setVisible={() => updateLinkSignatureToSystem(null)} />
|
||||
)}
|
||||
|
||||
<AddSystemDialog
|
||||
|
||||
@@ -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,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.CharIcon {}
|
||||
.CharIcon {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.CharRow {
|
||||
display: grid;
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 = {
|
||||
@@ -18,12 +18,8 @@ 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)));
|
||||
};
|
||||
|
||||
export const CharacterCard = ({
|
||||
@@ -47,7 +43,9 @@ export const CharacterCard = ({
|
||||
{!compact && (
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)` }}
|
||||
style={{
|
||||
backgroundImage: `url(https://images.evetech.net/characters/${char.eve_id}/portrait)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col flex-grow">
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, 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',
|
||||
@@ -29,11 +17,7 @@ export interface TooltipProps {
|
||||
offset?: number;
|
||||
content: (() => React.ReactNode) | React.ReactNode;
|
||||
targetSelector?: string;
|
||||
}
|
||||
|
||||
export interface WdTooltipHandlers {
|
||||
show: MouseEventHandler;
|
||||
hide: MouseEventHandler;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
export interface OffsetPosition {
|
||||
@@ -41,169 +25,185 @@ 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: (e?: React.MouseEvent) => void;
|
||||
getIsMouseInside: () => boolean;
|
||||
}
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState<OffsetPosition | null>(null);
|
||||
const [ev, setEv] = useState<MouseEvent>();
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
export const WdTooltip = forwardRef(
|
||||
(props: TooltipProps & { className?: string }, ref: ForwardedRef<WdTooltipHandlers>) => {
|
||||
const {
|
||||
content,
|
||||
targetSelector,
|
||||
position: tPosition = TooltipPosition.default,
|
||||
className,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
} = props;
|
||||
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [pos, setPos] = useState<OffsetPosition | null>(null);
|
||||
const [ev, setEv] = useState<React.MouseEvent>();
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const [isMouseInsideTooltip, setIsMouseInsideTooltip] = useState(false);
|
||||
|
||||
if (!tooltipRef.current) {
|
||||
const calcTooltipPosition = useCallback(({ x, y }: { x: number; y: number }) => {
|
||||
if (!tooltipRef.current) return { left: x, top: y };
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
let newLeft = x;
|
||||
let newTop = y;
|
||||
|
||||
if (newLeft < 0) newLeft = 10;
|
||||
if (newTop < 0) newTop = 10;
|
||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
return { left: newLeft, top: newTop };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tooltipWidth = tooltipRef.current.offsetWidth;
|
||||
const tooltipHeight = tooltipRef.current.offsetHeight;
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: (mouseEvt?: React.MouseEvent) => {
|
||||
if (mouseEvt) setEv(mouseEvt);
|
||||
setPos(null);
|
||||
setVisible(true);
|
||||
},
|
||||
hide: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
getIsMouseInside: () => isMouseInsideTooltip,
|
||||
}));
|
||||
|
||||
if (newLeft < 0) {
|
||||
newLeft = 10;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !ev) return;
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const { clientX, clientY, target } = ev;
|
||||
const targetBounds = (target as HTMLElement).getBoundingClientRect();
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 10;
|
||||
}
|
||||
let offsetX = clientX;
|
||||
let offsetY = clientY;
|
||||
|
||||
if (newLeft + tooltipWidth + 10 > window.innerWidth) {
|
||||
newLeft = window.innerWidth - tooltipWidth - 10;
|
||||
}
|
||||
if (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
return { left: newLeft, top: newTop };
|
||||
}, []);
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
const tooltipBounds = tooltipEl.getBoundingClientRect();
|
||||
offsetX = targetBounds.left - tooltipBounds.width - offset;
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipBounds.height / 2;
|
||||
if (offsetX <= 0) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
}
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !ev) {
|
||||
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) {
|
||||
if (tPosition === TooltipPosition.right) {
|
||||
offsetX = targetBounds.left + targetBounds.width + offset;
|
||||
}
|
||||
|
||||
setPosition(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
show: e => {
|
||||
setEv(e);
|
||||
setVisible(true);
|
||||
setPosition(null);
|
||||
},
|
||||
hide: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (targetSelector == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const targetElement = e.target as HTMLElement;
|
||||
|
||||
if (!targetElement) {
|
||||
setVisible(false);
|
||||
offsetY = targetBounds.y + targetBounds.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesFound = [...(targetElement?.parentElement?.querySelectorAll(targetSelector) ?? [])];
|
||||
|
||||
if (!nodesFound.includes(targetElement)) {
|
||||
setVisible(false);
|
||||
if (tPosition === TooltipPosition.top) {
|
||||
offsetY = targetBounds.top - tooltipEl.offsetHeight - offset;
|
||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
|
||||
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 (newTop + tooltipHeight + 10 > window.innerHeight) {
|
||||
newTop = window.innerHeight - tooltipHeight - 10;
|
||||
}
|
||||
setPosition({ top: newTop, left: newLeft });
|
||||
if (tPosition === TooltipPosition.bottom) {
|
||||
offsetY = targetBounds.bottom + offset;
|
||||
offsetX = targetBounds.x + targetBounds.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const deb = debounce(handleMouseMove, 10);
|
||||
setPos(calcTooltipPosition({ x: offsetX, y: offsetY }));
|
||||
}, [calcTooltipPosition, ev, tPosition, offset]);
|
||||
|
||||
// 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);
|
||||
};
|
||||
}, [targetSelector]);
|
||||
useEffect(() => {
|
||||
if (!targetSelector) return;
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
function handleMouseMove(nativeEvt: globalThis.MouseEvent) {
|
||||
const targetEl = nativeEvt.target as HTMLElement | null;
|
||||
if (!targetEl) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
const triggerEl = targetEl.closest(targetSelector!);
|
||||
const isInsideTooltip = interactive && tooltipRef.current?.contains(targetEl);
|
||||
|
||||
if (!triggerEl && !isInsideTooltip) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
setVisible(true);
|
||||
|
||||
if (triggerEl && tooltipRef.current) {
|
||||
const rect = triggerEl.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
let x = nativeEvt.clientX;
|
||||
let y = nativeEvt.clientY;
|
||||
|
||||
if (tPosition === TooltipPosition.left) {
|
||||
x = rect.left - tooltipEl.offsetWidth - offset;
|
||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
if (x <= 0) {
|
||||
x = rect.left + rect.width + offset;
|
||||
}
|
||||
} else if (tPosition === TooltipPosition.right) {
|
||||
x = rect.left + rect.width + offset;
|
||||
y = rect.y + rect.height / 2 - tooltipEl.offsetHeight / 2;
|
||||
} else if (tPosition === TooltipPosition.top) {
|
||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.top - tooltipEl.offsetHeight - offset;
|
||||
} else if (tPosition === TooltipPosition.bottom) {
|
||||
x = rect.x + rect.width / 2 - tooltipEl.offsetWidth / 2;
|
||||
y = rect.bottom + offset;
|
||||
}
|
||||
|
||||
setPos(calcTooltipPosition({ x, y }));
|
||||
}
|
||||
}
|
||||
|
||||
const debounced = debounce(handleMouseMove, 10);
|
||||
|
||||
const listener: EventListener = evt => {
|
||||
debounced(evt as globalThis.MouseEvent);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', listener);
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', listener);
|
||||
};
|
||||
}, [targetSelector, interactive, tPosition, offset, calcTooltipPosition]);
|
||||
|
||||
return createPortal(
|
||||
visible && (
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute p-1 border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos === null ? 'invisible' : '',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
top: pos?.top ?? 0,
|
||||
left: pos?.left ?? 0,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
onMouseEnter={() => interactive && setIsMouseInsideTooltip(true)}
|
||||
onMouseLeave={() => interactive && setIsMouseInsideTooltip(false)}
|
||||
>
|
||||
{typeof content === 'function' ? content() : content}
|
||||
</div>
|
||||
),
|
||||
document.body,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
WdTooltip.displayName = 'WdTooltip';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,21 +35,73 @@
|
||||
.topLeft {
|
||||
top: -7.5px;
|
||||
left: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 7.5px;
|
||||
left: 7.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-left: 1px solid var(--window-corner);
|
||||
border-top: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.topRight {
|
||||
top: -7.5px;
|
||||
right: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 7.5px;
|
||||
right: -2.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 1px solid var(--window-corner);
|
||||
border-top: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomLeft {
|
||||
bottom: -7.5px;
|
||||
left: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 2.5px;
|
||||
left: 7.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-left: 1px solid var(--window-corner);
|
||||
border-bottom: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomRight {
|
||||
bottom: -7.5px;
|
||||
right: -7.5px;
|
||||
|
||||
&::after {
|
||||
position: relative;
|
||||
top: 2.5px;
|
||||
right: -2.5px;
|
||||
display: block;
|
||||
content: " ";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-right: 1px solid var(--window-corner);
|
||||
border-bottom: 1px solid var(--window-corner);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { WindowProps } from '@/hooks/Mapper/components/ui-kit/WindowManager/type
|
||||
|
||||
const MIN_WINDOW_SIZE = 100;
|
||||
const SNAP_THRESHOLD = 10;
|
||||
const SNAP_GAP = 10;
|
||||
export const SNAP_GAP = 10;
|
||||
|
||||
export enum ActionType {
|
||||
Drag = 'drag',
|
||||
@@ -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,13 +99,18 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
})),
|
||||
);
|
||||
|
||||
const refPrevSize = useRef({ w: 0, h: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
setWindows(
|
||||
initialWindows.map((window, index) => ({
|
||||
...window,
|
||||
zIndex: index + 1,
|
||||
})),
|
||||
);
|
||||
if (!viewPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
refPrevSize.current = viewPort;
|
||||
}, [viewPort]);
|
||||
|
||||
useEffect(() => {
|
||||
setWindows(initialWindows.slice(0));
|
||||
}, [initialWindows]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -107,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);
|
||||
}, []);
|
||||
|
||||
@@ -341,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 };
|
||||
}
|
||||
|
||||
@@ -389,6 +403,14 @@ export const WindowManager: React.FC<WindowManagerProps> = ({ windows: initialWi
|
||||
next.position.y = container.clientHeight - next.size.height - SNAP_GAP;
|
||||
}
|
||||
|
||||
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,14 +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 { WidgetsIds } from '@/hooks/Mapper/components/mapInterface/constants.tsx';
|
||||
import {
|
||||
ToggleWidgetVisibility,
|
||||
useStoreWidgets,
|
||||
WindowStoreInfo,
|
||||
} from '@/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts';
|
||||
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 = {
|
||||
@@ -24,11 +37,12 @@ const INITIAL_DATA: MapRootData = {
|
||||
routes: undefined,
|
||||
kills: [],
|
||||
connections: [],
|
||||
|
||||
detailedKills: {},
|
||||
selectedSystems: [],
|
||||
selectedConnections: [],
|
||||
userPermissions: {},
|
||||
options: {},
|
||||
linkSignatureToSystem: null,
|
||||
};
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
@@ -64,21 +78,16 @@ export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
theme: 'default',
|
||||
};
|
||||
|
||||
export const STORED_VISIBLE_WIDGETS_DEFAULT = [
|
||||
WidgetsIds.info,
|
||||
WidgetsIds.local,
|
||||
WidgetsIds.routes,
|
||||
WidgetsIds.signatures,
|
||||
];
|
||||
|
||||
export interface MapRootContextProps {
|
||||
update: ContextStoreDataUpdate<MapRootData>;
|
||||
data: MapRootData;
|
||||
outCommand: OutCommandHandler;
|
||||
interfaceSettings: InterfaceStoredSettings;
|
||||
setInterfaceSettings: Dispatch<SetStateAction<InterfaceStoredSettings>>;
|
||||
windowsVisible: WidgetsIds[];
|
||||
setWindowsVisible: Dispatch<SetStateAction<WidgetsIds[]>>;
|
||||
windowsSettings: WindowStoreInfo;
|
||||
toggleWidgetVisibility: ToggleWidgetVisibility;
|
||||
updateWidgetSettings: WindowsManagerOnChange;
|
||||
resetWidgets: () => void;
|
||||
}
|
||||
|
||||
const MapRootContext = createContext<MapRootContextProps>({
|
||||
@@ -112,10 +121,7 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
|
||||
defaultValue: STORED_INTERFACE_DEFAULT_VALUES,
|
||||
},
|
||||
);
|
||||
|
||||
const [windowsVisible, setWindowsVisible] = useLocalStorageState<WidgetsIds[]>('windows:visible', {
|
||||
defaultValue: STORED_VISIBLE_WIDGETS_DEFAULT,
|
||||
});
|
||||
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } = useStoreWidgets();
|
||||
|
||||
useEffect(() => {
|
||||
let foundNew = false;
|
||||
@@ -143,8 +149,10 @@ export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProvide
|
||||
outCommand: outCommand,
|
||||
setInterfaceSettings,
|
||||
interfaceSettings,
|
||||
windowsVisible,
|
||||
setWindowsVisible,
|
||||
windowsSettings,
|
||||
updateWidgetSettings,
|
||||
toggleWidgetVisibility,
|
||||
resetWidgets,
|
||||
}}
|
||||
>
|
||||
<MapRootHandlers ref={fwdRef}>{children}</MapRootHandlers>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandAddSystems, CommandRemoveSystems, CommandUpdateSystems } from '@/hooks/Mapper/types';
|
||||
import {
|
||||
CommandAddSystems,
|
||||
CommandRemoveSystems,
|
||||
CommandUpdateSystems,
|
||||
CommandLinkSignatureToSystem,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
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;
|
||||
@@ -74,5 +80,28 @@ export const useCommandsSystems = () => {
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
return { addSystems, removeSystems, updateSystems, updateSystemSignatures };
|
||||
const updateLinkSignatureToSystem = useCallback(async (command: CommandLinkSignatureToSystem) => {
|
||||
const { update } = ref.current;
|
||||
update({ linkSignatureToSystem: command }, true);
|
||||
}, []);
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CommandCharactersUpdated,
|
||||
CommandCharacterUpdated,
|
||||
CommandInit,
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
@@ -29,10 +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 } = useCommandsSystems();
|
||||
const {
|
||||
addSystems,
|
||||
removeSystems,
|
||||
updateSystems,
|
||||
updateSystemSignatures,
|
||||
updateLinkSignatureToSystem,
|
||||
updateDetailedKills,
|
||||
} = useCommandsSystems();
|
||||
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
|
||||
const { charactersUpdated, characterAdded, characterRemoved, characterUpdated, presentCharacters } =
|
||||
useCommandsCharacters();
|
||||
@@ -93,7 +102,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
// do nothing here
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
@@ -108,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;
|
||||
|
||||
121
assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts
Normal file
121
assets/js/hooks/Mapper/mapRootProvider/hooks/useStoreWidgets.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import {
|
||||
CURRENT_WINDOWS_VERSION,
|
||||
DEFAULT_WIDGETS,
|
||||
STORED_VISIBLE_WIDGETS_DEFAULT,
|
||||
WidgetsIds,
|
||||
WINDOWS_LOCAL_STORE_KEY,
|
||||
} 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, 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;
|
||||
|
||||
export const getDefaultWidgetProps = () => ({
|
||||
version: CURRENT_WINDOWS_VERSION,
|
||||
visible: STORED_VISIBLE_WIDGETS_DEFAULT,
|
||||
windows: DEFAULT_WIDGETS,
|
||||
});
|
||||
|
||||
export const useStoreWidgets = () => {
|
||||
const [windowsSettings, setWindowsSettings] = useLocalStorageState<WindowStoreInfo>(WINDOWS_LOCAL_STORE_KEY, {
|
||||
defaultValue: getDefaultWidgetProps(),
|
||||
});
|
||||
|
||||
const ref = useRef({ windowsSettings, setWindowsSettings });
|
||||
ref.current = { windowsSettings, setWindowsSettings };
|
||||
|
||||
const updateWidgetSettings: WindowsManagerOnChange = useCallback(({ windows, viewPort }) => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
setWindowsSettings(({ version, visible /*, windows*/ }: WindowStoreInfo) => {
|
||||
return {
|
||||
version,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
windows: DEFAULT_WIDGETS.map(({ content, ...x }) => {
|
||||
const windowProp = windows.find(j => j.id === x.id);
|
||||
if (windowProp) {
|
||||
return windowProp;
|
||||
}
|
||||
|
||||
return x;
|
||||
}),
|
||||
viewPort,
|
||||
visible,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleWidgetVisibility: ToggleWidgetVisibility = useCallback(widgetId => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
setWindowsSettings(({ visible, windows, ...x }) => {
|
||||
const isCheckedPrev = visible.includes(widgetId);
|
||||
if (!isCheckedPrev) {
|
||||
const maxZIndex = Math.max(...windows.map(w => w.zIndex));
|
||||
return {
|
||||
...x,
|
||||
windows: windows.map(wnd => {
|
||||
if (wnd.id === widgetId) {
|
||||
return { ...wnd, position: { x: SNAP_GAP, y: SNAP_GAP }, zIndex: maxZIndex + 1 };
|
||||
}
|
||||
|
||||
return wnd;
|
||||
}),
|
||||
visible: [...visible, widgetId],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...x,
|
||||
windows,
|
||||
visible: visible.filter(x => x !== widgetId),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { setWindowsSettings } = ref.current;
|
||||
|
||||
const raw = localStorage.getItem(WINDOWS_LOCAL_STORE_KEY);
|
||||
if (!raw) {
|
||||
console.warn('No windows found in local storage!!');
|
||||
|
||||
setWindowsSettings(getDefaultWidgetProps());
|
||||
return;
|
||||
}
|
||||
|
||||
const { version, windows, visible, viewPort } = JSON.parse(raw) as WindowStoreInfo;
|
||||
if (!version || CURRENT_WINDOWS_VERSION > version) {
|
||||
setWindowsSettings(getDefaultWidgetProps());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-debugger
|
||||
const out = windows.filter(x => DEFAULT_WIDGETS.find(def => def.id === x.id));
|
||||
|
||||
setWindowsSettings({
|
||||
version: CURRENT_WINDOWS_VERSION,
|
||||
windows: out as WindowProps[],
|
||||
visible,
|
||||
viewPort,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetWidgets = useCallback(() => ref.current.setWindowsSettings(getDefaultWidgetProps()), []);
|
||||
|
||||
return {
|
||||
windowsSettings,
|
||||
updateWidgetSettings,
|
||||
toggleWidgetVisibility,
|
||||
resetWidgets,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -118,6 +123,7 @@ export enum OutCommand {
|
||||
deleteHub = 'delete_hub',
|
||||
getRoutes = 'get_routes',
|
||||
getCharacterJumps = 'get_character_jumps',
|
||||
getStructures = 'get_structures',
|
||||
getSignatures = 'get_signatures',
|
||||
getSystemStaticInfos = 'get_system_static_infos',
|
||||
getConnectionInfo = 'get_connection_info',
|
||||
@@ -127,6 +133,7 @@ export enum OutCommand {
|
||||
updateConnectionShipSizeType = 'update_connection_ship_size_type',
|
||||
updateConnectionLocked = 'update_connection_locked',
|
||||
updateConnectionCustomInfo = 'update_connection_custom_info',
|
||||
updateStructures = 'update_structures',
|
||||
updateSignatures = 'update_signatures',
|
||||
updateSystemName = 'update_system_name',
|
||||
updateSystemTemporaryName = 'update_system_temporary_name',
|
||||
@@ -147,6 +154,10 @@ export enum OutCommand {
|
||||
openUserSettings = 'open_user_settings',
|
||||
getPassages = 'get_passages',
|
||||
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',
|
||||
@@ -157,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>;
|
||||
|
||||
@@ -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',
|
||||
@@ -117,6 +118,7 @@ export type SolarSystemRawType = {
|
||||
status: number;
|
||||
name: string | null;
|
||||
temporary_name: string | null;
|
||||
linked_sig_eve_id: string | null;
|
||||
|
||||
system_static_info: SolarSystemStaticInfoRaw;
|
||||
system_signatures: SystemSignature[];
|
||||
@@ -130,4 +132,3 @@ export type SearchSystemItem = {
|
||||
system_static_info: SolarSystemStaticInfoRaw;
|
||||
value: number;
|
||||
};
|
||||
|
||||
|
||||
BIN
assets/static/images/news/01-13-theme-swap/faoble-theme.png
Executable file
BIN
assets/static/images/news/01-13-theme-swap/faoble-theme.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
BIN
assets/static/images/news/01-13-theme-swap/theme-selector.png
Executable file
BIN
assets/static/images/news/01-13-theme-swap/theme-selector.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/static/images/news/01-20-structure-widget/cover.png
Executable file
BIN
assets/static/images/news/01-20-structure-widget/cover.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/static/images/news/01-20-structure-widget/enable-widget.png
Executable file
BIN
assets/static/images/news/01-20-structure-widget/enable-widget.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
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 |
@@ -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")
|
||||
@@ -64,7 +69,19 @@ map_subscription_characters_limit =
|
||||
|
||||
map_subscription_hubs_limit =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 100)
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 10)
|
||||
|
||||
map_subscription_base_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_BASE_PRICE", 100_000_000)
|
||||
|
||||
map_subscription_extra_characters_100_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_CHARACTERS_100_PRICE", 50_000_000)
|
||||
|
||||
map_subscription_extra_hubs_10_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
||||
|
||||
map_connection_auto_expire_hours =
|
||||
config_dir
|
||||
@@ -76,7 +93,7 @@ map_connection_auto_eol_hours =
|
||||
|
||||
map_connection_eol_expire_timeout_mins =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_EOL_EXPIRE_TIMEOUT_MINS", 30)
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_EOL_EXPIRE_TIMEOUT_MINS", 60)
|
||||
|
||||
wallet_tracking_enabled =
|
||||
config_dir
|
||||
@@ -101,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,
|
||||
@@ -117,16 +135,16 @@ config :wanderer_app,
|
||||
},
|
||||
%{
|
||||
id: "omega",
|
||||
characters_limit: 300,
|
||||
hubs_limit: 20,
|
||||
base_price: 250_000_000,
|
||||
characters_limit: map_subscription_characters_limit * 2,
|
||||
hubs_limit: map_subscription_hubs_limit * 2,
|
||||
base_price: map_subscription_base_price,
|
||||
month_3_discount: 0.2,
|
||||
month_6_discount: 0.4,
|
||||
month_12_discount: 0.5
|
||||
}
|
||||
],
|
||||
extra_characters_100: 75_000_000,
|
||||
extra_hubs_10: 25_000_000
|
||||
extra_characters_100: map_subscription_extra_characters_100_price,
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||
}
|
||||
|
||||
config :ueberauth, Ueberauth,
|
||||
|
||||
@@ -16,6 +16,7 @@ defmodule WandererApp.Api do
|
||||
resource WandererApp.Api.MapState
|
||||
resource WandererApp.Api.MapSystem
|
||||
resource WandererApp.Api.MapSystemSignature
|
||||
resource WandererApp.Api.MapSystemStructure
|
||||
resource WandererApp.Api.MapCharacterSettings
|
||||
resource WandererApp.Api.MapSubscription
|
||||
resource WandererApp.Api.MapTransaction
|
||||
|
||||
@@ -127,7 +127,6 @@ defmodule WandererApp.Api.Map do
|
||||
update :update_api_key do
|
||||
accept [:public_api_key]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
172
lib/wanderer_app/api/map_system_structure.ex
Normal file
172
lib/wanderer_app/api/map_system_structure.ex
Normal file
@@ -0,0 +1,172 @@
|
||||
defmodule WandererApp.Api.MapSystemStructure do
|
||||
@moduledoc """
|
||||
Ash resource representing a structure in a given map system.
|
||||
|
||||
"""
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_system_structures_v1")
|
||||
end
|
||||
|
||||
code_interface do
|
||||
define(:all_active, action: :all_active)
|
||||
define(:create, action: :create)
|
||||
define(:update, action: :update)
|
||||
|
||||
define(:by_id,
|
||||
get_by: [:id],
|
||||
action: :read
|
||||
)
|
||||
|
||||
define(:by_system_id,
|
||||
action: :by_system_id,
|
||||
args: [:system_id]
|
||||
)
|
||||
end
|
||||
|
||||
actions do
|
||||
default_accept [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
defaults [:read, :destroy]
|
||||
|
||||
read :all_active do
|
||||
prepare build(sort: [updated_at: :desc])
|
||||
end
|
||||
|
||||
read :by_system_id do
|
||||
argument :system_id, :string, allow_nil?: false
|
||||
filter(expr(system_id == ^arg(:system_id)))
|
||||
end
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
|
||||
accept [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
argument :system_id, :uuid, allow_nil?: false
|
||||
|
||||
change manage_relationship(:system_id, :system,
|
||||
on_lookup: :relate,
|
||||
on_no_match: nil
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
update :update do
|
||||
primary? true
|
||||
require_atomic? false
|
||||
|
||||
accept [
|
||||
:system_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :structure_type_id, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :structure_type, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :solar_system_name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :solar_system_id, :integer do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :owner_name, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :owner_ticker, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :owner_id, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :status, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :end_time, :utc_datetime_usec do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
update_timestamp :updated_at
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :system, WandererApp.Api.MapSystem do
|
||||
attribute_writable? true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -95,7 +95,9 @@ defmodule WandererApp.Api.UserActivity do
|
||||
:map_acl_member_updated,
|
||||
:map_connection_added,
|
||||
:map_connection_updated,
|
||||
:map_connection_removed
|
||||
:map_connection_removed,
|
||||
:signatures_added,
|
||||
:signatures_removed
|
||||
]
|
||||
)
|
||||
|
||||
@@ -108,8 +110,6 @@ defmodule WandererApp.Api.UserActivity do
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
|
||||
|
||||
relationships do
|
||||
belongs_to :character, WandererApp.Api.Character do
|
||||
allow_nil? true
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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}",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user